Commit db23883
feat(tools): 统一工具调用接口,offline + realtime 多 provider (#1035)
* feat(tools): 统一工具调用接口,offline + realtime 多 provider 支持
新增 ToolRegistry / ToolDefinition / ToolCall / ToolResult 作为 provider-agnostic
基础类型(main_logic/tool_calling.py),LLMSessionManager 持有 registry 并
向 OmniOfflineClient / OmniRealtimeClient 透传 on_tool_call 回调与 tool 列表。
Provider 协议覆盖:
- Offline OpenAI-compat(含 Anthropic / OpenRouter / Qwen-text):ChatOpenAI
扩展 tools/tool_choice 透传 + 流式 tool_call_deltas 累积
- Offline Gemini:迁移到 google-genai SDK 直连,OpenAI-compat 作为 fallback
- Realtime OpenAI gpt:flat schema + response.done.output[type=function_call]
- Realtime Gemini Live:types.Tool(function_declarations) + send_tool_response
- Realtime GLM:flat schema,function_call_output 不带 call_id(按文档),
mid-session 更新 tools 时同时传 turn_detection
- Realtime StepFun + free-lanlan.tech:嵌套 function 形,保留 web_search
- Realtime Qwen Omni:嵌套 function 形(按 Aliyun 最新文档),与 enable_search
互斥时自动关闭
跨进程开放:main_routers/tool_router.py 提供 /api/tools/{register,unregister,
clear} HTTP 端点供 plugin / agent_server 注册带 callback_url 的远程工具,
ToolRegistry.remote_dispatcher 在执行时 POST 转发。
TODO 留待后续:
- Realtime free-lanlan.app(Vertex AI Live 代理,需服务端配合)
- Offline lanlan.app 国际版(Gemini OpenAI-compat 端点丢弃 tools 字段)
测试:tests/unit/test_tool_calling.py 16 项,覆盖 registry 本地/远程派发、
streaming delta 聚合、offline tool loop 端到端、迭代上限、各 provider
wire-format(Qwen 嵌套 vs GLM flat、GLM 不带 call_id、Qwen enable_search
互斥、GLM mid-session turn_detection、StepFun web_search 共存)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): 清理 unused imports + 空 except 加注释
- tests/unit/test_tool_calling.py:删掉测试文件没用上的 asyncio / types as _types
imports(pytest.mark.asyncio 不需要直接 import asyncio)。
- main_logic/omni_offline_client.py:genai usage_metadata 的兜底 except 加上
debug-log 与意图说明 —— usage 是可选 telemetry,SDK 字段差异不应打断主文本流。
回应 PR #1035 上 github-code-quality bot 的三条 inline 反馈。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): switch_model 重算 genai 路由 + tool 历史带 function name
回应 PR #1035 Codex 两条反馈:
P1 (omni_offline_client.py:417): switch_model 之前只改了 self.model / self.llm,
没同步 self.base_url / self.api_key,也没重算 _use_genai_sdk。结果:
conversation 是 OpenAI、vision 是 Gemini 的场景,第一次发图切到 vision 后
路由旗标还停在 OpenAI-compat,把 Gemini 请求错送到 OpenAI 路径。修:
- switch_model 同步 base_url / api_key
- 重算 _use_genai_sdk
- 清空 _genai_client(api_key 可能变了,强制 lazy init)
- 重置 _genai_tools_unsupported
P2 (omni_offline_client.py:222): _execute_and_append_openai_tool_calls
append 的 tool 消息没带 name 字段,转 Gemini Content 时只能 fallback 到
tool_call_id("call_xxx"),Gemini server 看到 FunctionResponse.name 不
匹配原 function_call.name 会丢弃这条结果。修:
- append tool 消息时一并写入 name 字段
- _genai_messages_to_contents 的 tool 分支:先用 msg.name,再反查前一条
assistant tool_calls 找 tool_call_id 对应的 function name,最后才占位。
绝不把 call_xxx 当函数名给 Gemini。
测试:补 test_offline_switch_model_recomputes_genai_routing 回归保护,
test_offline_openai_path_runs_tool_then_text 断言 history 带 name。
共 17/17 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): pending_session 同步 + 空 name 防御 + timeout 上下界
回应 PR #1035 CodeRabbit 三条反馈(剩 2 条与 Codex P1/P2 重复,已在 3d4a39b 修过):
1. core.py _sync_tools_to_active_session 仅同步 self.session,
pending_session(热切换预热中)拿不到 mid-conversation 注册的工具。
修:覆盖两个 session,OmniRealtimeClient 仅在 ws 已连接时才 apply_tools。
2. utils/llm_client.py ChatOpenAI.collect_tool_calls 不过滤空 name 槽位。
SDK 偶发流出残缺碎片会被原样写进 tool_calls 历史,下一轮调用被
server schema reject。修:drop empty-name 并 warning。
omni_offline_client._execute_and_append_openai_tool_calls 入口加同样的
防御性过滤,万一外部直接构造也兜得住。
3. main_routers/tool_router.py ToolRegisterRequest.timeout_seconds 没
上下界。误填超大值会卡死整条 tool-call 路径。修:Field(gt=0, le=300)。
测试:补 test_collect_tool_calls_drops_empty_name_fragments 回归保护,
共 18/18 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(tools): 抽 _make_rt_client helper 减少 realtime 测试初始化重复
回应 PR #1035 CodeRabbit nitpick:5 个 OmniRealtimeClient wire-format 测试
里 __new__ + 7 个字段赋值 + send_event 桩的初始化模板逐字一致,抽成
_make_rt_client(api_type, *, tool_name, tool_kwargs) 后,单测从 ~220 行
缩到 ~140 行;后续 client 加新内部状态只需改一处。
行为完全保留——18/18 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): connect 后补 sync + 路由本地校验 + genai client 优雅关闭等 5 处
回应 PR #1035 CodeRabbit 第 3 轮 review 6 条反馈:
1. core.py 主 session / pending_session 的 connect() 后各补一次
_sync_tools_to_active_session()。修构造时拍快照与 connect 期间
register_tool 的 race window:之前的异步 fire_task 可能还没看到
self.session 就跑完,新连上的 session 会带旧 toolset 直到下次工具变更。
2. main_routers/tool_router.py:所有端点挂上 verify_local_access
依赖(复用 cookies_login_router 已有实现),避免服务暴露到 LAN 时
被远程注册任意 callback_url。
3. main_routers/tool_router.py register 的全失败分支:之前永远返回
ok=True,全失败时插件会误以为已注册。改为 affected 为空时返回
ok=False + failed_roles 详情,部分成功时同时带回 failed_roles。
4. omni_offline_client.py _genai_messages_to_contents:tool 消息查
不到 function name 时改成 continue 跳过,不再用 "unknown_tool"
占位(占位会让 Gemini 拿到孤儿 tool result,无效还浪费 token)。
5. omni_offline_client.py switch_model + close:旧 _genai_client
提早 close()(SDK 只有同步 close 没 aclose,用 to_thread 不阻事件循环),
及时释放底层 httpx 连接池。
6. tests/unit/test_tool_calling.py: switch_model 路由重算回归测试
monkeypatch _GENAI_AVAILABLE=True,避免没装 google-genai 的 CI
静默 skip。
18/18 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): final swap promote 后补 sync + transient 错误别永久禁用 genai
回应 PR #1035 CodeRabbit 第 4 轮 review 2 条反馈:
1. core.py _perform_final_swap_sequence 在 self.session = new_session
之后立刻补一次 _sync_tools_to_active_session()。swap 序列里
pending_session → 局部 new_session → self.session 跨了几个 await,
期间 register_tool 触发的 _sync 既找不到 pending_session(已被挪走)
也找不到 self.session(还没赋值),导致 promote 后新 session 缺
那次注册的工具。
2. omni_offline_client.py _astream_genai_with_tools 不再把
generate_content_stream 的所有异常 wrap 成 _GenaiToolsUnsupported。
原实现会让 transient 错误(429/5xx/网络抖动/auth 临时失败)触发
_astream_with_tools 永久翻 _genai_tools_unsupported=True,整个
session 后续都退化到 OpenAI-compat(且 OpenAI-compat 不支持 Gemini
工具)。改为只在错误消息含 tool/function + not_support/unsupported/
invalid 关键字组合时才认定 tools 不支持,其余异常直接 raise,让上层
except Exception 分支临时 fallback 但下一轮还会重试 genai。
测试:补 test_offline_genai_transient_error_does_not_disable_tools
和 test_offline_genai_tools_unsupported_error_correctly_disables_path
做对偶验证。20/20 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): 同轮 text+tool_call 历史完整 + stream 异常判断收紧
回应 PR #1035 CodeRabbit 第 5 轮 review:
1. omni_offline_client.py _astream_genai_with_tools 同一 Gemini turn 里
text part 与 function_call part 并存时,写 assistant 历史的 content
不再固定 "",改为累积本轮已 yield 给用户的 text。否则下一轮 LLM
看上下文会缺掉前半句,模型重复 / 改口,最终持久化历史的顺序也跟
真实生成顺序对不上。
2. 同文件流过程异常判断收紧到与 generate_content_stream 调用本身一致:
只有错误消息含 "tool/function" + "not_support/unsupported/invalid"
关键字组合时才永久翻盘禁用 genai 路径,其他流中异常(含
"function call timeout" 之类的 transient)原样 raise,让上层临时
fallback、下一轮再试。
测试:补 test_offline_genai_streamed_text_persisted_with_tool_call
回归保护。21/21 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): tools sync 串行 + Gemini 历史 text part 不丢
回应 PR #1035 CodeRabbit 第 6 轮 review 2 条:
1. core.py 新增 _tool_sync_lock 串行化 _sync_tools_to_active_session:
防止连续 register / unregister / clear 触发的 session.update 在 wire
上乱序,最后一份快照可能不对应 registry 最终状态。新增
register_tool_and_sync / unregister_tool_and_sync / clear_tools_and_sync
await 版本,HTTP /api/tools 端点改用这条路径,让 caller 拿到 ok=True
时 session 已真生效,不再有"返回成功但下次 model 调用看不到工具"的窗口。
同步入口 register_tool 仍保留 fire-and-forget 兜底。
2. omni_offline_client.py _genai_messages_to_contents 在 assistant 同时带
content + tool_calls 时,先 emit text part 再 emit function_call parts。
原实现只保留 function_call parts,把上一轮才修复的 streamed_text_buffer
写进历史的 text 又扔了,下一轮 LLM 还是看不到已 stream 出去的前半句。
测试:补 test_genai_messages_to_contents_preserves_text_with_tool_calls
+ test_register_tool_and_sync_serializes_concurrent_updates。23/23 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): genai 已吐文本后不再静默 fallback,避免双流拼接
回应 PR #1035 CodeRabbit 第 7 轮 review actionable:
omni_offline_client.py _astream_with_tools 跟踪 ``genai_emitted_text``。
原实现:_astream_genai_with_tools 在已经 yield 过 text chunk 之后才抛
transient 异常时,落到 except 分支会继续 fallback 到 _astream_openai_with_tools,
用户在同一轮看到"半截 Gemini 文本 + 一份基于旧历史从头生成的 OpenAI
文本"双流拼接。
修:已吐文本后再抛异常 → 直接 raise,让 stream_text 的 retry / discard
流程清空气泡 + 通知 response_discarded,按 attempt+1 重试。
未吐文本 → 仍然静默 fallback(用户感知不到失败)。
测试:补 test_offline_no_silent_fallback_after_genai_emitted_text +
test_offline_silent_fallback_when_genai_did_not_emit 对偶验证两种路径,
25/25 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): _and_sync 真失败 + 角色隔离 + not_support 关键词补齐
回应 PR #1035 CodeRabbit 第 8 轮 review 3 条新 actionable:
1. core.py _sync_tools_to_active_session 加 raise_on_failure=True 选项,
register_tool_and_sync / unregister_tool_and_sync / clear_tools_and_sync
全部带这个参数。session.update 真在 wire 上失败时把异常往上抛,
避免 HTTP /api/tools 回 ok=true 假成功(之前同步失败只 log warning,
caller 看不到)。
2. main_routers/tool_router.py /api/tools/unregister 与 /clear 加单角色
失败隔离:try/except 单角色 sync 异常,收集到 failed_roles,已成功
的 role 进 affected_roles,跨角色调用不再因为某个 role 抛异常就
整体 500(与已对偶 register 端点一致)。
3. main_logic/omni_offline_client.py "tools 永久不支持"判定关键词补
"not_support"(下划线变体)。原本只覆盖 "not support"(带空格),
API 错误消息出现 "function_call_not_support" 这种下划线写法时会被
误判 transient,导致每轮先撞 genai 再回退的额外抖动。
(第 7 轮在 inline 上重复发的 "已有文本输出后别静默 fallback" 已在
7cc67f1 修过,这里不重复。)
测试:补
- test_register_tool_and_sync_propagates_session_update_failure
- test_unregister_tool_router_isolates_per_role_failures
- test_genai_unsupported_keyword_matches_underscore_variant
28/28 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): stream_text 已吐文本后失败也要 notify discarded 清气泡
回应 PR #1035 CodeRabbit 第 9 轮新增 inline (614):
omni_offline_client.py stream_text 通用 except Exception 分支之前只
上报 TEXT_GEN_ERROR 状态消息就 break,没调用 _notify_response_discarded。
这意味着第 7 轮 (7cc67f1) 引入的 "_astream_with_tools 已吐文本后 raise"
新契约其实没被上层真正接住——前端那截半截 Gemini 文本气泡永远停在那。
修:通用 except Exception 分支识别 ``assistant_message`` 已被填过的
场景(说明已经吐过文本给前端),调 _notify_response_discarded(
reason="text_gen_error:<type>", will_retry=False, message=
'{"code":"TEXT_GEN_ERROR_AFTER_PARTIAL", ...}')让前端清空气泡。
和 (APIConnectionError 等) 分支的"吐过文本就 notify discarded"语义对偶,
区别在 will_retry=False(通用 except 不再重试)。
测试:补 test_stream_text_notifies_discarded_when_partial_text_then_error
模拟 _astream_with_tools 吐文本后 raise,验证 stream_text 调
_notify_response_discarded 而不是只上报 TEXT_GEN_ERROR。29/29 通过。
(本轮另外 3 条都是第 8 轮已修问题的 inline 重发,6ee28120 已处理。)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): OpenAI-compat 路径同 turn text+tool_calls 历史也保留 text
回应 PR #1035 CodeRabbit 第 10 轮 inline 530:
omni_offline_client.py _execute_and_append_openai_tool_calls 加 assistant_text
参数;_astream_openai_with_tools 累积 streamed_text_buffer 并传给 helper。
和 Gemini 路径完全对偶——某些 OpenAI-compat provider(GLM-text / Qwen-text
等)会在同一 assistant turn 里"先吐文字再进 tool_calls"。原来 content
固定 "" 会让下一轮 LLM 看不到自己已说过的前半句,模型重复 / 改口。
测试补 test_offline_openai_path_persists_streamed_text_with_tool_calls,
30/30 通过。
(本轮另外 2 条 duplicate (core.py 串行 await sync / Gemini 历史 text part)
都是更早 commit 已修过的,4fd4b633 已处理。)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): GenAI 路径与 OpenAI 路径对偶补齐空 name function_call 防御
回应 PR #1035 CodeRabbit 第 11 轮 inline 791:
omni_offline_client.py _astream_genai_with_tools 在收集 function_call 时
检查 name 非空。OpenAI 路径已在 collect_tool_calls 出口 + _execute_and_
append_openai_tool_calls 入口做了双重防御,GenAI 路径之前直接信任
fn_call.name 会让空字符串传到 on_tool_call,并把无效 tool_calls 写回
messages,下一轮 generate_content_stream 收到无名 function_call 直接
schema reject。drop + warning 与 OpenAI 路径完全对偶。
测试:补 test_offline_genai_path_drops_empty_name_function_calls,模拟
同 turn 里一个有效 + 一个空 name 的 function_call,断言 handler 只被
有效那个调用、写回历史也只有有效那个。31/31 通过。
(本轮另外 30 多条 inline 都是 CodeRabbit 把历史 review 一次性重发的
ack,对应 commits c115aa4..6ee2812 已修过,无新动作。)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): set_tools 重置 unsupported 旗标 + stream_text 防 pre-tool 文本双写
回应 PR #1035 CodeRabbit 第 12 轮 2 条 inline:
1. omni_offline_client.py set_tools 顺手清掉 _genai_tools_unsupported。
原本这个旗标因旧工具集触发 GenerateContentConfig rejected / unsupported
异常被翻 True 后永远停留,热卸载坏 schema 工具也救不回 native genai 路径,
只能等下次 connect()/switch_model() 重置。set_tools 既然是工具列表
的"换血"入口,应该一并给 genai 一次重新尝试的机会。
2. omni_offline_client.py stream_text + _astream_*_with_tools 修复
pre-tool 文本双写 history。原结构里 _astream_*_with_tools 在 tool 轮
结束时已经 inline 把 (assistant{content: pre-tool 文本, tool_calls},
tool result) 写进 _conversation_history,但 stream_text 同时把整段
累积的 assistant_message(pre-tool + post-tool)作为 final AIMessage
再 append 一次——pre-tool 文本被双写。
修法:utils/llm_client.py LLMStreamChunk 加 tool_round_persisted
sentinel 字段,两个 _astream_*_with_tools 在持久化 tool 轮后 yield
LLMStreamChunk(content="", tool_round_persisted=True)。stream_text
收到 sentinel 时把 final-segment buffer (assistant_message) 清掉,
pipe_count / prefix_buffer 也跟着重置(下一段是新语义单元)。
同时新增 assistant_message_total 累计完整一轮文本,长度 guard 与
_check_repetition 仍看 total,与人类用户感知的"这一轮 AI 说了什么"
一致。final AIMessage append 只用 final-segment。
测试:补
- test_set_tools_resets_genai_unsupported_flag
- test_stream_text_does_not_double_write_pretool_text
33/33 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): stream_text 整轮级判定统一切到 assistant_message_total
回应 PR #1035 CodeRabbit 第 13 轮 inline 1159:
70cf761 引入 sentinel 后 assistant_message 语义改成 "post-tool
final-segment buffer",但还有 6 处"整轮级"分支仍看它,导致 tool 轮已
yield pre-tool 文本时这些分支会误判"本轮没出过文本"。统一切到
assistant_message_total:
- flush 块的长度 guard(count_tokens)—— 与主累加块对偶
- recovery 路径的 _is_gibberish_response / truncate_to_tokens / 日志
- retry 循环外的 "本轮成功完成" break 判定(避免 max_tool_iterations
耗尽时只剩 pre-tool 但被错误重试)
- (APIConnectionError 等) retry 分支的 "已吐文本要 notify_discarded
清气泡" 判定(pre-tool 残留前端时漏触发会让半截气泡停在那)
- 通用 except Exception 分支的同一处判定
- finally 末 LLM_NO_RESPONSE 判定(tool 轮跑完但模型没出 final 文本
时不应该报"无回复")
附带:APIConnectionError retry 分支同步重置 assistant_message_total
(之前只重置了 final-segment)。stream_text 入口与函数顶部都把两个
buffer 一并初始化,避免 finally 看到 NameError。
final AIMessage append (1407-1412) 仍用 final-segment——这是 sentinel
设计的核心:pre-tool 已经在 assistant.tool_calls.content 持久化,final
AIMessage 不再双写。
测试:33/33 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(plugins): 新增 LLM 工具调用注册教学(中文)
针对 PR #1035 引入的 /api/tools/* 端点,给插件作者写完整的注册指南:
- 架构与端点 schema (register / unregister / clear / list)
- callback_url 协议(请求体 / 响应体格式 + ToolResult 字段)
- 完整 lifecycle pattern(启动重试 + shutdown unregister)
- main_server 重启时的工具丢失风险(registry 是内存对象)
- 切换猫娘的 role 字段语义
- 同进程注册(绕过 HTTP)的高级用法
- 注意事项(敏感信息、本机限制、timeout 上限、错误返回约定等)
放在 docs/zh-CN/plugins/tool-calling.md,并在 plugins/index.md 加目录链接。
英文 / 日文翻译留待后续 i18n 同步 PR。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tools): callback_url host 白名单防 SSRF + 修文档措辞
回应 PR #1035 CodeRabbit 第 14 轮 inline (tool-calling.md:238):
文档原措辞"callback_url 必须是 127.0.0.1/localhost 由 verify_local_access
守门"是错的——后者只管"谁能调注册端点",不校验 callback_url 值本身。
本地 caller 完全可以注册一个公网 callback_url 把 main_server 当 SSRF
出站代理对外发 LLM 工具调用 payload(含用户对话内容、模型 args)。
加 host 白名单:
- main_routers/tool_router.py 新增 _validate_local_callback_url 函数,
ToolRegisterRequest.callback_url 用 field_validator 调它做校验。
实现:urlparse 拆 scheme/host,scheme 限 http(s),host 用
ipaddress.ip_address 检 is_loopback(覆盖 127.0.0.0/8、::1、
IPv4-mapped IPv6 等),同时认 localhost 字面量。非 loopback / 非 IP
/ 错误 scheme 都直接 422。
- docs/zh-CN/plugins/tool-calling.md 改成"两道独立闸门"措辞,把
verify_local_access 与 callback_url 白名单的语义分开说清楚。
测试:补 test_tool_register_request_rejects_non_loopback_callback_url,
覆盖合法 (127.0.0.x / localhost / ::1 / https) 和非法(公网 IP / 局域网
IP / 私有域名 / 错误 scheme / 缺 host / 公网 IPv6)case。34/34 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(plugins): 补 tool-calling 教学的英文 / 日文版本
f9a0ae3 只加了中文版,英文 / 日文用户读到 plugins/index.md 不会看到
这条目录链接、单独路径下也是 404。补齐 docs/plugins/tool-calling.md
(英文)和 docs/ja/plugins/tool-calling.md(日文),结构一致:架构图、
端点 schema、callback_url 协议、完整 lifecycle pattern、main_server
重启行为、role 字段语义、同进程注册、注意事项(含两道独立闸门 SSRF
防护说明)。两个语言的 plugins/index.md 也一并加目录链接。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(plugins): 修 tool-calling 教学的 3 处接口描述错误
回应 PR #1035 CodeRabbit 第 15 轮 inline 反馈,3 处都成立,3 个语言版本
(zh-CN / en / ja)一并修:
1. /api/tools/clear 文档原写"source 为空时清空全部"——错。
ToolClearRequest.source = Field(..., min_length=1) 是必填,空值会被
422 拒绝;"全部清空"语义只在内部 mgr.clear_tools(source=None) 才有。
改成明确"source 必填,需要全清要走内部 API 或按 source 逐个 clear"。
2. callback_url 响应体 output 字段映射写反——_remote_dispatch 实际是
body.get("output", body),含 "output" key 时取值,否则取整 body。
原文档说"main_server 把整个 body 作 ToolResult.output"会让插件作者
把 is_error/error 跟实际结果同层平铺,结果模型看不到 metadata。改
成"始终用 {"output": ...} 包一层" + 解释抽取规则。
3. 同进程注册示例里 ToolDefinition(source="...") 不存在这个参数——
ToolDefinition 只有 name/description/parameters/handler/metadata 5 个
字段,source 标签按 dataclass docstring 应放进 metadata={"source":
"..."}。照原示例 copy 会直接 TypeError。修示例和注释。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent db7d671 commit db23883
14 files changed
Lines changed: 4495 additions & 65 deletions
File tree
- docs
- ja/plugins
- plugins
- zh-CN/plugins
- main_logic
- main_routers
- tests/unit
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
| 83 | + | |
83 | 84 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| 88 | + | |
88 | 89 | | |
0 commit comments