From 482f8df5081bf4c8d8d76512e88ffa5e775324b4 Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Tue, 21 Jul 2026 15:43:39 +0800 Subject: [PATCH] add first commit --- .gitignore | 11 ++++++ __init__.py | 0 doubao/__init__.py | 39 ++++++++++++++++++++ doubao/global_var.py | 17 +++++++++ doubao/models/__init__.py | 14 ++++++++ doubao/models/pydantic.py | 23 ++++++++++++ doubao/router/__init__.py | 3 ++ doubao/router/analyze.py | 48 +++++++++++++++++++++++++ example/__init__.py | 50 ++++++++++++++++++++++++++ example/global_var.py | 24 +++++++++++++ example/models/__init__.py | 32 +++++++++++++++++ example/models/counter.py | 14 ++++++++ example/models/pydantic.py | 74 ++++++++++++++++++++++++++++++++++++++ example/router/__init__.py | 14 ++++++++ example/router/add.py | 52 +++++++++++++++++++++++++++ example/router/count.py | 33 +++++++++++++++++ example/router/divide.py | 29 +++++++++++++++ example/router/multiply.py | 22 ++++++++++++ example/router/subtract.py | 22 ++++++++++++ 19 files changed, 521 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 doubao/__init__.py create mode 100644 doubao/global_var.py create mode 100644 doubao/models/__init__.py create mode 100644 doubao/models/pydantic.py create mode 100644 doubao/router/__init__.py create mode 100644 doubao/router/analyze.py create mode 100644 example/__init__.py create mode 100644 example/global_var.py create mode 100644 example/models/__init__.py create mode 100644 example/models/counter.py create mode 100644 example/models/pydantic.py create mode 100644 example/router/__init__.py create mode 100644 example/router/add.py create mode 100644 example/router/count.py create mode 100644 example/router/divide.py create mode 100644 example/router/multiply.py create mode 100644 example/router/subtract.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3b3022 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +build_nuitka/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/doubao/__init__.py b/doubao/__init__.py new file mode 100644 index 0000000..a833cc4 --- /dev/null +++ b/doubao/__init__.py @@ -0,0 +1,39 @@ +""" +豆包视觉理解 Worker 插件 +""" +import os +from volcenginesdkarkruntime import AsyncArk +from chongming_worker import create_workers as _create_workers +from chongming_worker import auto_bind_models +from .global_var import set_ark_client, cleanup_ark_client +from .router import * + +# 自动绑定 gen_models 生成的 Pydantic 模型 +auto_bind_models("doubao") + +async def create_workers(session, config, count=1, worker_manager=None): + """ + 工厂函数:由 app 生命周期调用,创建 Worker 协程。 + """ + # 从配置或环境变量获取 API Key + api_key = config.get("api_key") or os.environ.get("VOLC_ACCESSKEY") + if not api_key: + raise ValueError("缺少 API_KEY,请在配置中设置 api_key 或环境变量 VOLC_ACCESSKEY") + + base_url = config.get("base_url", "https://ark.cn-beijing.volces.com/api/v3") + model = config.get("model", "doubao-seed-evolving") + + # 将模型名存入环境变量,供 router 使用 + os.environ["DOUBAO_MODEL"] = model + + # 创建全局客户端 + client = AsyncArk(base_url=base_url, api_key=api_key) + set_ark_client(client) + + # 委托给通用运行时创建 Worker 任务 + return _create_workers(session, config, count, worker_manager) + +async def cleanup(worker_item: str): + """清理资源""" + cleanup_ark_client() + print(f"{worker_item} 清理完毕") \ No newline at end of file diff --git a/doubao/global_var.py b/doubao/global_var.py new file mode 100644 index 0000000..ad7a56e --- /dev/null +++ b/doubao/global_var.py @@ -0,0 +1,17 @@ +from typing import Optional +from volcenginesdkarkruntime import AsyncArk + +_ark_client: Optional[AsyncArk] = None + +def set_ark_client(client: AsyncArk): + global _ark_client + _ark_client = client + +def get_ark_client() -> Optional[AsyncArk]: + return _ark_client + +def cleanup_ark_client(): + global _ark_client + if _ark_client: + # AsyncArk 无显式 close,但可设为 None + _ark_client = None \ No newline at end of file diff --git a/doubao/models/__init__.py b/doubao/models/__init__.py new file mode 100644 index 0000000..6955df0 --- /dev/null +++ b/doubao/models/__init__.py @@ -0,0 +1,14 @@ +""" +模型导出 — doubao +自动生成,请勿手动修改 +""" + +from .pydantic import AnalyzePostRequest +from .pydantic import AnalyzePostResponse +from .pydantic import DoubaoResponse + +__all__ = [ + "AnalyzePostRequest", + "AnalyzePostResponse", + "DoubaoResponse", +] diff --git a/doubao/models/pydantic.py b/doubao/models/pydantic.py new file mode 100644 index 0000000..e3528ae --- /dev/null +++ b/doubao/models/pydantic.py @@ -0,0 +1,23 @@ +""" +自动生成的 Pydantic 模型 — doubao +来源: config/workers/doubao/ +""" + +from __future__ import annotations + +from typing import Any, Optional +from pydantic import BaseModel, Field + +class AnalyzePostRequest(BaseModel): + """分析图片内容 — 请求体""" + image_base64: str = Field(...) + prompt: str = Field(...) + +class AnalyzePostResponse(BaseModel): + """分析图片内容 — 响应体""" + result: str = Field(...) + error: Optional[str] = Field(default='') + + +# 所有响应模型的联合类型 +DoubaoResponse = AnalyzePostResponse diff --git a/doubao/router/__init__.py b/doubao/router/__init__.py new file mode 100644 index 0000000..fb843d7 --- /dev/null +++ b/doubao/router/__init__.py @@ -0,0 +1,3 @@ +from .analyze import analyze + +__all__ = ["analyze"] \ No newline at end of file diff --git a/doubao/router/analyze.py b/doubao/router/analyze.py new file mode 100644 index 0000000..ee7164d --- /dev/null +++ b/doubao/router/analyze.py @@ -0,0 +1,48 @@ +import os +from chongming_worker import rpc_method +from ..models.pydantic import AnalyzePostRequest, AnalyzePostResponse +from ..global_var import get_ark_client + +@rpc_method( + subject="doubao.analyze", + method="POST", + summary="Analyze image via Doubao vision model", + response_model=AnalyzePostResponse, +) +async def analyze(request: AnalyzePostRequest) -> AnalyzePostResponse: + print(f"[DEBUG] analyze called with prompt={request.prompt}, image_base64 length={len(request.image_base64)}") + client = get_ark_client() + if client is None: + return AnalyzePostResponse(result="", error="客户端未初始化") + + # 获取图片数据 + image_base64 = request.image_base64 + + if not image_base64: + return AnalyzePostResponse(result="", error="未提供图片数据(image_base64或image_path)") + + if image_base64.startswith('data:image/'): + image_base64 = image_base64.split(',', 1)[1] + + mime_type = "image/png" + + prompt = request.prompt or "" + + try: + response = await client.chat.completions.create( + model=os.getenv("DOUBAO_MODEL", "doubao-seed-evolving"), + messages=[ + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": f"data:{mime_type};base64,{image_base64}"}, + {"type": "text", "text": prompt} + ] + } # type: ignore + ], + thinking={"type": "disabled"} + ) + result = response.choices[0].message.content # type: ignore + return AnalyzePostResponse(result=result) # type: ignore + except Exception as e: + return AnalyzePostResponse(result="", error=f"调用豆包API失败: {str(e)}") \ No newline at end of file diff --git a/example/__init__.py b/example/__init__.py new file mode 100644 index 0000000..154690d --- /dev/null +++ b/example/__init__.py @@ -0,0 +1,50 @@ +""" +Example Worker 插件 — 演示 @rpc_method + 显式模型绑定用法 + +本插件展示如何使用 chongming_worker 框架定义 RPC 端点。 + +核心工作流: + 1. 编写 config/workers/example/*.toml 配置文件 + 2. 执行 uv run gen_models 生成 Pydantic 模型 + 3. 在 __init__.py 中用 @rpc_method 定义业务函数 + 4. 显式导入模型并绑定到对应端点(替代 auto_bind_models) + +关键约定: + - 模型命名: {Request|Response} + (如 AddGetRequest, AddGetResponse 由 gen_models 自动生成) + - 装饰器 subject 中的操作名 → 模型前缀(驼峰) + example.add → Add, example.subtract → Subtract +""" +import modelvault +from pathlib import Path + +from chongming_worker import auto_bind_models +from chongming_worker.runtime import create_workers as _create_workers + +from .router import * + +auto_bind_models("example") + + +# ============================================================ +# 工厂函数 — 供 app lifespan 动态加载 +# ============================================================ +# 框架通过 __import__ + getattr 获取此函数, +# 然后调用 create_workers(session, config, count, worker_manager=worker_manager)。 +# 详见 src/chongming/app/__init__.py 的 lifespan。 + + +async def create_workers(session, config, count=1, worker_manager=None): + """插件入口 — 委托给 chongming_worker 运行时创建 Worker 协程""" + from .global_var import set_global_var + + db_path = Path(config.get("data_dir", "./data")) / "example.modelvault" + db_path.parent.mkdir(parents=True, exist_ok=True) + db = await modelvault.AsyncDatabase.open(str(db_path)) # type: ignore + set_global_var("db", db) + + return _create_workers(session, config, count, worker_manager) + +async def cleanup(worker_item: str): + print(f"{worker_item}关闭") + pass diff --git a/example/global_var.py b/example/global_var.py new file mode 100644 index 0000000..9b45137 --- /dev/null +++ b/example/global_var.py @@ -0,0 +1,24 @@ +import sys +from typing import Any, Callable, Awaitable, List +import modelvault + +# 初始定义(可以保留 db 作为示例) +db: modelvault.AsyncDatabase | None = None # type: ignore + +def set_global_var(name: str, value: Any) -> None: + """ + 设置当前模块的全局变量。 + + Args: + name: 变量名(字符串) + value: 任意值 + """ + globals()[name] = value + + + +# 但这样会污染导出列表,不推荐自动添加。 +__all__ = [ + "db", + "set_global_var", +] \ No newline at end of file diff --git a/example/models/__init__.py b/example/models/__init__.py new file mode 100644 index 0000000..6860567 --- /dev/null +++ b/example/models/__init__.py @@ -0,0 +1,32 @@ +""" +模型导出 — example +自动生成,请勿手动修改 +""" + +from .pydantic import AddGetRequest +from .pydantic import AddGetResponse +from .pydantic import AddPostRequest +from .pydantic import AddPostResponse +from .pydantic import CountGetResponse +from .pydantic import DivideGetRequest +from .pydantic import DivideGetResponse +from .pydantic import ExampleResponse +from .pydantic import MultiplyGetRequest +from .pydantic import MultiplyGetResponse +from .pydantic import SubtractGetRequest +from .pydantic import SubtractGetResponse + +__all__ = [ + "AddGetRequest", + "AddGetResponse", + "AddPostRequest", + "AddPostResponse", + "CountGetResponse", + "DivideGetRequest", + "DivideGetResponse", + "ExampleResponse", + "MultiplyGetRequest", + "MultiplyGetResponse", + "SubtractGetRequest", + "SubtractGetResponse", +] diff --git a/example/models/counter.py b/example/models/counter.py new file mode 100644 index 0000000..9e6b2e4 --- /dev/null +++ b/example/models/counter.py @@ -0,0 +1,14 @@ +# src/plugins/example/models/counter.py +from pydantic import BaseModel +import modelvault + +class Counter(BaseModel): + # 主键字段 + __modelvault_primary_key__ = "name" + # 唯一索引 + __modelvault_indexes__ = [ + modelvault.models.unique("name"), + ] + + name: str # 计数器名称(例如 "visits") + value: int = 0 \ No newline at end of file diff --git a/example/models/pydantic.py b/example/models/pydantic.py new file mode 100644 index 0000000..41114be --- /dev/null +++ b/example/models/pydantic.py @@ -0,0 +1,74 @@ +""" +自动生成的 Pydantic 模型 — example +来源: config/workers/example/ +""" + +from __future__ import annotations + +from typing import Any, Optional +from pydantic import BaseModel, Field + +class AddGetRequest(BaseModel): + """Add two numbers — 请求体""" + a: float = Field(...) + b: float = Field(...) + +class AddGetResponse(BaseModel): + """Add two numbers — 响应体""" + result: float = Field(...) + operation: str = Field(default='add') + timestamp: Optional[float] = Field(default=0.0) + +class AddPostRequest(BaseModel): + """Add two numbers (POST) — 请求体""" + a: float = Field(...) + b: float = Field(...) + +class AddPostResponse(BaseModel): + """Add two numbers (POST) — 响应体""" + result: float = Field(...) + operation: str = Field(default='add') + timestamp: Optional[float] = Field(default=0.0) + +class CountGetResponse(BaseModel): + """Get visit count and increment — 响应体""" + name: str = Field(...) + value: int = Field(...) + +class DivideGetRequest(BaseModel): + """Divide two numbers — 请求体""" + a: float = Field(...) + b: float = Field(...) + +class DivideGetResponse(BaseModel): + """Divide two numbers — 响应体""" + result: Optional[float] = Field(default=0.0) + operation: str = Field(default='divide') + timestamp: Optional[float] = Field(default=0.0) + error: Optional[str] = Field(default='') + +class MultiplyGetRequest(BaseModel): + """Multiply two numbers — 请求体""" + a: float = Field(...) + b: float = Field(...) + +class MultiplyGetResponse(BaseModel): + """Multiply two numbers — 响应体""" + result: float = Field(...) + operation: str = Field(default='multiply') + timestamp: Optional[float] = Field(default=0.0) + +class SubtractGetRequest(BaseModel): + """Subtract two numbers — 请求体""" + a: float = Field(...) + b: float = Field(...) + +class SubtractGetResponse(BaseModel): + """Subtract two numbers — 响应体""" + result: float = Field(...) + operation: str = Field(default='subtract') + timestamp: Optional[float] = Field(default=0.0) + + +# 所有响应模型的联合类型 +ExampleResponse = SubtractGetResponse | CountGetResponse | MultiplyGetResponse | AddPostResponse | AddGetResponse | DivideGetResponse diff --git a/example/router/__init__.py b/example/router/__init__.py new file mode 100644 index 0000000..087cce9 --- /dev/null +++ b/example/router/__init__.py @@ -0,0 +1,14 @@ +from .add import add, add_post +from .subtract import subtract +from .multiply import multiply +from .divide import divide +from .count import count_get + +__all__ = [ + "add", + "add_post", + "subtract", + "multiply", + "divide", + "count_get" +] \ No newline at end of file diff --git a/example/router/add.py b/example/router/add.py new file mode 100644 index 0000000..364c1f9 --- /dev/null +++ b/example/router/add.py @@ -0,0 +1,52 @@ +import time + +import zenoh + +from chongming_worker import rpc_method +from chongming_request import Client + +from ..models.pydantic import ( + AddGetRequest, + AddGetResponse, + AddPostRequest, + AddPostResponse +) + +@rpc_method( + subject="example.add", + method="GET", + summary="Add two numbers via GET", + response_model=AddGetResponse, +) +async def add(add_get_request:AddGetRequest) -> AddGetResponse: + """加法运算(GET)— 通过查询参数传入 a、b,返回它们的和""" + return AddGetResponse( + result=add_get_request.a + add_get_request.b, + operation="add", + timestamp=time.time() + ) + +@rpc_method( + subject="example.add", + method="POST", + summary="Add two numbers via POST", + response_model=AddPostResponse, +) +async def add_post(add_post_request: AddPostRequest, session: zenoh.Session) -> AddPostResponse: + """加法运算(POST)— 通过 JSON Body 传入 a、b,返回它们的和""" + async with Client(session=session) as client: + add_result = await client.call( + worker_type="example", + subject="example.add", + payload={ + "a":add_post_request.a, + "b":add_post_request.b + }, + method="GET" + ) + print(add_result) + return AddPostResponse( + result=add_post_request.a + add_post_request.b, + operation="add", + timestamp=time.time(), + ) diff --git a/example/router/count.py b/example/router/count.py new file mode 100644 index 0000000..92c237c --- /dev/null +++ b/example/router/count.py @@ -0,0 +1,33 @@ +import modelvault + +from chongming_worker import rpc_method + +from ..models.pydantic import CountGetResponse +from ..models.counter import Counter + + +@rpc_method( + subject="example.count", + method="GET", + summary="Increment and get counter value", + response_model=CountGetResponse, # 需要定义 CountResponse +) +async def count_get() -> CountGetResponse: + from ..global_var import db + + # 确保数据库已初始化(如果未初始化,则同步初始化,但这里假设已就绪) + if db is None: + raise RuntimeError("Database not initialized") + + counters = modelvault.models.async_collection(db, Counter) # type: ignore + async with db.transaction(): + counter = await counters.get("visits") + if counter is None: + counter = Counter(name="visits", value=0) + await counters.insert(counter) + else: + new_value = counter.value + 1 + await counters.update("visits", {"value": new_value}) + counter.value = new_value # 更新本地对象以便返回 + + return CountGetResponse(name=counter.name, value=counter.value) # type: ignore diff --git a/example/router/divide.py b/example/router/divide.py new file mode 100644 index 0000000..44d136f --- /dev/null +++ b/example/router/divide.py @@ -0,0 +1,29 @@ +import time + +from chongming_worker import rpc_method + +from ..models.pydantic import ( + DivideGetResponse, + DivideGetRequest, +) + +@rpc_method( + subject="example.divide", + method="GET", + summary="Divide two numbers", + response_model=DivideGetResponse, +) +async def divide(divide_get_request: DivideGetRequest) -> DivideGetResponse: + """除法运算:a / b(b 不能为 0)""" + if divide_get_request.b == 0: + return DivideGetResponse( + error="除数不能为零", + operation="divide", + result=0.0, + timestamp=time.time(), + ) + return DivideGetResponse( + result=divide_get_request.a / divide_get_request.b, + operation="divide", + timestamp=time.time(), + ) \ No newline at end of file diff --git a/example/router/multiply.py b/example/router/multiply.py new file mode 100644 index 0000000..ff082f9 --- /dev/null +++ b/example/router/multiply.py @@ -0,0 +1,22 @@ +import time + +from chongming_worker import rpc_method + +from ..models.pydantic import ( + MultiplyGetRequest, + MultiplyGetResponse, +) + +@rpc_method( + subject="example.multiply", + method="GET", + summary="Multiply two numbers", + response_model=MultiplyGetResponse, +) +async def multiply(multiply_get_request=MultiplyGetRequest) -> MultiplyGetResponse: + """乘法运算:a * b""" + return MultiplyGetResponse( + result=multiply_get_request.a * multiply_get_request.b, + operation="multiply", + timestamp=time.time(), + ) \ No newline at end of file diff --git a/example/router/subtract.py b/example/router/subtract.py new file mode 100644 index 0000000..1c8b7fb --- /dev/null +++ b/example/router/subtract.py @@ -0,0 +1,22 @@ +import time + +from chongming_worker import rpc_method + +from ..models.pydantic import ( + SubtractGetResponse, + SubtractGetRequest, +) + +@rpc_method( + subject="example.subtract", + method="GET", + summary="Subtract two numbers", + response_model=SubtractGetResponse, +) +async def subtract(subtract_get_request: SubtractGetRequest) -> SubtractGetResponse: + """减法运算:a - b""" + return SubtractGetResponse( + result=subtract_get_request.a - subtract_get_request.b, + operation="subtract", + timestamp=time.time(), + ) \ No newline at end of file