mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 01:18:08 +00:00
feat(lua plugin): align Lua plugin bridge with C ABI surface, accept table args in sdk.memory.recall
- Lua 桥接层补齐 register_def/no_memory/cleaner/own_tools/output+input channels/register_api/set_auto_restart 等 v0.8.0 能力,与 C ABI 插件面一致 - sdk.memory.recall 兼容表参数(多 key 批量 recall),修复 Lua 插件传表报 bad argument - 新增 TestLuaAlignedAPIs 覆盖对齐后的 API 面(含 recall 数组回归断言) - sdk.lua 同步子表 mock(memory/doc/knowledge/text_memory/llm/social/settings) - 更新 PLUGIN_DEV.md 中英文示例插件参考表
This commit is contained in:
@ -292,7 +292,7 @@ Tool output → valuable for LLM attention?
|
||||
└── No → Normal memory, no extra handling
|
||||
```
|
||||
|
||||
> **Note**: `Cleaner` is a Go `func` type (`json:"-"`), cannot cross C ABI boundaries. Not available for Lua plugins or remote plugins.
|
||||
> **Note**: `Cleaner` is a Go `func` type (`json:"-"`), cannot cross C ABI boundaries, so it is unavailable for C/C++/Rust remote plugins. **Lua plugins are not affected**: pass a Lua function in the def table (`cleaner = function(text) return text end`) — the Go bridge calls it back per invocation during memory computation.
|
||||
|
||||
#### Stage Hooks — Intervene in message processing flow
|
||||
|
||||
@ -538,22 +538,49 @@ When running inside the kernel, `sdk.*` global variables are injected by the Go
|
||||
|
||||
### Lua SDK API
|
||||
|
||||
The `sdk.*` API of Lua plugins is fully aligned with external plugins (C ABI / toolchain-built `.so`/`.dll`): registration functions raise a Lua error on failure; data functions uniformly return `(result, err)` with `err == nil` on success. Subsystems not wired by the core (e.g. SocialAPI) return empty values instead of errors.
|
||||
|
||||
**Registration**
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `sdk.log(level, msg)` | Log output |
|
||||
| `sdk.register_tool(name, def, handler)` | Register tool |
|
||||
| `sdk.register_stage(stage, handler)` | Register stage hook |
|
||||
| `sdk.register_tool(name, def, handler)` | Register tool; `def` supports `description`, `parameters`, `no_memory`, `cleaner` |
|
||||
| `sdk.register_stage(stage, handler, scope)` | Register stage hook; `scope` is `nil`/`"global"` (default) or `"own_tools"` (fires only for `before_toolcall`/`after_toolcall` when the tool belongs to this plugin) |
|
||||
| `sdk.register_api(name)` | Register API |
|
||||
| `sdk.get_setting(key)` | Read config |
|
||||
| `sdk.set_setting(key, value)` | Write config |
|
||||
| `sdk.register_output_channel(name, caps, desc, def, handler)` | Register output channel; `def` supports `no_memory`, `cleaner` |
|
||||
| `sdk.register_input_channel(name, def)` | Register input channel; `def` as above |
|
||||
| `sdk.set_auto_restart(enabled)` | Auto-restart the plugin after a crash |
|
||||
|
||||
**Stage hook context**
|
||||
|
||||
Stage handlers receive the full context (same as external plugins): `raw_message`, `user_id`, `group_id`, `phase`, `llm_text`, `final_text`, `no_memory`, `response` (when responded), `tool_calls`, `tool_results`.
|
||||
|
||||
**IO and config**
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `sdk.get_setting(key)` / `sdk.set_setting(key, value)` | Own plugin config read/write |
|
||||
| `sdk.settings.get_core/set_core/list_core(key)` | Core config read/write |
|
||||
| `sdk.settings.get_plugin/set_plugin/list_plugin(plugin, key)` | Other plugin config read/write |
|
||||
| `sdk.settings.list/defs/dump/plugins(prefix)` | Config queries |
|
||||
| `sdk.settings.register_def(def)` | Register config definition (WebUI display) |
|
||||
| `sdk.inject_text(source, channel, text)` | Deliver text message |
|
||||
| `sdk.inject_interrupt(source, channel, text)` | Interrupt delivery |
|
||||
| `sdk.json.encode(val)` | JSON encode |
|
||||
| `sdk.json.decode(str)` | JSON decode |
|
||||
| `sdk.http.get(url)` | HTTP GET request (`-- !impl`) |
|
||||
| `sdk.http.post(url, body, content_type)` | HTTP POST request (`-- !impl`) |
|
||||
| `sdk.inject_text_no_memory(source, channel, text)` | Deliver without memory computation |
|
||||
|
||||
> **Note**: Lua plugin's `sdk.register_stage` callback currently only receives `raw_message`, `user_id`, `phase` fields. The functionality is limited. For complex stage handling logic, use Go plugins.
|
||||
**Data APIs (aligned with C ABI, all return `(result, err)`)**
|
||||
|
||||
| Sub-table | Functions |
|
||||
|-----------|-----------|
|
||||
| `sdk.memory.*` | `recall(query, depth)`, `commit({triples})`, `introspect()`, `merge(source, target)`, `purge(criteria, hard)` |
|
||||
| `sdk.doc.*` | `query(text, top_k)`, `insert({id,title,content})`, `remove(id)`, `stats()` |
|
||||
| `sdk.knowledge.*` | `search(query, limit)`, `add(tag, content)`, `list()` |
|
||||
| `sdk.text_memory.*` | `append({role,content,timestamp,channel})` |
|
||||
| `sdk.llm.*` | `list_sources()`, `set_source(name)`, `current_source()` |
|
||||
| `sdk.social.*` (read-only) | `get_person(name)`, `get_network(name, depth)`, `get_trait(name, trait)`, `get_relations(name)`, `list_persons()` |
|
||||
| `sdk.json.*` | `encode(val)`, `decode(str)` |
|
||||
| `sdk.http.*` | `get(url)`, `post(url, body, content_type)` |
|
||||
|
||||
---
|
||||
|
||||
@ -679,15 +706,21 @@ Internal: records are stored in SQLite `disabled_plugins` table (`name`, `disabl
|
||||
|
||||
| Example | Type | Features |
|
||||
|---------|------|----------|
|
||||
| [weather](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/weather) | Go | Weather queries (wttr.in); demonstrates NoMemory/Cleaner/stage hooks/channels/text memory |
|
||||
| [luademo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/luademo) | Lua | Full-featured Lua example covering the whole v0.8.0 Lua SDK surface |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot integration, 17 tools, full input/output channel wiring |
|
||||
| [memo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/memo) | Go | Memo management, PreAction injection + timed interrupt dual reminder |
|
||||
| [files](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/files) | Go | File system operations, 4 write modes, sandbox isolation |
|
||||
| [browser](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/browser) | Go | Web search + HTTP fetch (SSRF) + Chromium render (merged from web/webfetch) |
|
||||
| [bili](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/bili) | Go | Bilibili video download (yt-dlp) |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot integration, 17 tools |
|
||||
| [editdoc](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/editdoc) | Go | Office document editing and format conversion |
|
||||
| [a2a](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/a2a) | Go | Agent-to-Agent protocol |
|
||||
| [ocr](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ocr) | Go | Offline text recognition (Tesseract) |
|
||||
| [sanitizer](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/sanitizer) | Go | Output sanitizer filter |
|
||||
| [calendar](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/calendar) | Go | Calendar management |
|
||||
| [rss](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/rss) | Go | RSS subscriptions |
|
||||
| [ai_image](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ai_image) | Go | AI image generation |
|
||||
| [music](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/music) | Go | Music playback |
|
||||
|
||||
### Built-in Plugins
|
||||
|
||||
|
||||
@ -290,7 +290,7 @@ s.RegisterTool("weather_query", sdk.ToolDef{
|
||||
└── 否 → 正常记忆,无需额外处理
|
||||
```
|
||||
|
||||
> **注意**:`Cleaner` 是 Go `func` 类型(`json:"-"`),不能跨 C ABI 边界序列化。Lua 插件和远程插件无法使用。
|
||||
> **注意**:`Cleaner` 是 Go `func` 类型(`json:"-"`),不能跨 C ABI 边界序列化,因此 C/C++/Rust 等远程插件无法使用。**Lua 插件不受此限**:def 表中直接传 Lua 函数即可(`cleaner = function(text) return text end`),Go 桥接层会在计算层调用时逐次回调 Lua。
|
||||
|
||||
#### 阶段钩子 — 干预消息处理流
|
||||
|
||||
@ -536,22 +536,49 @@ lua main.lua
|
||||
|
||||
### Lua SDK API
|
||||
|
||||
Lua 插件的 `sdk.*` API 与外部插件(C ABI / 工具链编译的 `.so`/`.dll`)能力完全对齐:注册类函数调用即时报错(抛 Lua error),数据类函数统一返回 `(result, err)`,`err` 为 nil 表示成功。核心未装配的子系统(如 SocialAPI)返回空值而非报错。
|
||||
|
||||
**注册类**
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `sdk.log(level, msg)` | 日志输出 |
|
||||
| `sdk.register_tool(name, def, handler)` | 注册工具 |
|
||||
| `sdk.register_stage(stage, handler)` | 注册阶段钩子 |
|
||||
| `sdk.register_tool(name, def, handler)` | 注册工具;`def` 支持 `description`、`parameters`、`no_memory`、`cleaner` |
|
||||
| `sdk.register_stage(stage, handler, scope)` | 注册阶段钩子;`scope` 为 `nil`/`"global"`(默认)或 `"own_tools"`(仅 `before_toolcall`/`after_toolcall` 且工具属于本插件时触发) |
|
||||
| `sdk.register_api(name)` | 注册 API |
|
||||
| `sdk.get_setting(key)` | 读取配置 |
|
||||
| `sdk.set_setting(key, value)` | 写入配置 |
|
||||
| `sdk.register_output_channel(name, caps, desc, def, handler)` | 注册输出通道;`def` 支持 `no_memory`、`cleaner` |
|
||||
| `sdk.register_input_channel(name, def)` | 注册输入通道;`def` 同上 |
|
||||
| `sdk.set_auto_restart(enabled)` | 崩溃时内核自动拉起插件 |
|
||||
|
||||
**阶段钩子上下文**
|
||||
|
||||
`register_stage` 的 handler 收到完整上下文(与外部插件一致):`raw_message`、`user_id`、`group_id`、`phase`、`llm_text`、`final_text`、`no_memory`、`response`(已响应时)、`tool_calls`、`tool_results`。
|
||||
|
||||
**IO 与配置**
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `sdk.get_setting(key)` / `sdk.set_setting(key, value)` | 本插件配置读写 |
|
||||
| `sdk.settings.get_core/set_core/list_core(key)` | 核心配置读写 |
|
||||
| `sdk.settings.get_plugin/set_plugin/list_plugin(plugin, key)` | 其他插件配置读写 |
|
||||
| `sdk.settings.list/defs/dump/plugins(prefix)` | 配置查询 |
|
||||
| `sdk.settings.register_def(def)` | 注册配置项定义(WebUI 展示) |
|
||||
| `sdk.inject_text(source, channel, text)` | 投递文本消息 |
|
||||
| `sdk.inject_interrupt(source, channel, text)` | 中断投递 |
|
||||
| `sdk.json.encode(val)` | JSON 编码 |
|
||||
| `sdk.json.decode(str)` | JSON 解码 |
|
||||
| `sdk.http.get(url)` | HTTP GET 请求(`-- !impl`) |
|
||||
| `sdk.http.post(url, body, content_type)` | HTTP POST 请求(`-- !impl`) |
|
||||
| `sdk.inject_text_no_memory(source, channel, text)` | 免记忆投递 |
|
||||
|
||||
> **注意**:Lua 插件的 `sdk.register_stage` 阶段回调目前仅传递 `raw_message`、`user_id`、`phase` 三个字段,功能受限。复杂的阶段处理逻辑建议使用 Go 插件。
|
||||
**数据类(与 C ABI 对齐,均返回 `(result, err)`)**
|
||||
|
||||
| 子表 | 函数 |
|
||||
|------|------|
|
||||
| `sdk.memory.*` | `recall(query, depth)`、`commit({triples})`、`introspect()`、`merge(source, target)`、`purge(criteria, hard)` |
|
||||
| `sdk.doc.*` | `query(text, top_k)`、`insert({id,title,content})`、`remove(id)`、`stats()` |
|
||||
| `sdk.knowledge.*` | `search(query, limit)`、`add(tag, content)`、`list()` |
|
||||
| `sdk.text_memory.*` | `append({role,content,timestamp,channel})` |
|
||||
| `sdk.llm.*` | `list_sources()`、`set_source(name)`、`current_source()` |
|
||||
| `sdk.social.*`(只读) | `get_person(name)`、`get_network(name, depth)`、`get_trait(name, trait)`、`get_relations(name)`、`list_persons()` |
|
||||
| `sdk.json.*` | `encode(val)`、`decode(str)` |
|
||||
| `sdk.http.*` | `get(url)`、`post(url, body, content_type)` |
|
||||
|
||||
---
|
||||
|
||||
@ -677,15 +704,21 @@ pmgr.ReloadPlugins() // 重载所有插件
|
||||
|
||||
| 示例 | 类型 | 特点 |
|
||||
|------|------|------|
|
||||
| [weather](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/weather) | Go | 天气查询(wttr.in),演示 NoMemory/Cleaner/阶段钩子/通道/文本记忆 |
|
||||
| [luademo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/luademo) | Lua | Lua 全功能示例,覆盖 v0.8.0 Lua SDK 全部 API 面 |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot 对接,17 个工具,输入/输出通道完整对接 |
|
||||
| [memo](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/memo) | Go | 备忘管理,PreAction 注入 + 定时打断双提醒 |
|
||||
| [files](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/files) | Go | 文件系统操作,4 种写入模式,沙箱隔离 |
|
||||
| [browser](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/browser) | Go | 网络搜索、网页抓取(SSRF)、浏览器渲染(合并自 web/webfetch) |
|
||||
| [bili](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/bili) | Go | B 站视频下载(yt-dlp) |
|
||||
| [qq](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/qq) | Go | NapCat OneBot 对接,17 个工具 |
|
||||
| [editdoc](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/editdoc) | Go | Office 文档编辑与格式转换 |
|
||||
| [a2a](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/a2a) | Go | Agent-to-Agent 协议 |
|
||||
| [ocr](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ocr) | Go | 离线文字识别(Tesseract) |
|
||||
| [sanitizer](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/sanitizer) | Go | 输出清洗过滤器 |
|
||||
| [calendar](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/calendar) | Go | 日历管理 |
|
||||
| [rss](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/rss) | Go | RSS 订阅 |
|
||||
| [ai_image](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/ai_image) | Go | AI 图片生成 |
|
||||
| [music](https://gitcode.com/JianFeeeee/homeagent-sdk/tree/main/example/music) | Go | 音乐播放 |
|
||||
|
||||
### 内置插件
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ function sdk.log(level, msg)
|
||||
end
|
||||
|
||||
-- !impl
|
||||
-- def: { description="...", parameters={...} }
|
||||
-- def: { description="...", parameters={...}, no_memory=true/false, cleaner=function(text)->text }
|
||||
-- handler: function(args) -> result
|
||||
function sdk.register_tool(name, def, handler)
|
||||
print("[lua-plugin] register_tool: " .. tostring(name))
|
||||
@ -21,8 +21,9 @@ end
|
||||
|
||||
-- !impl
|
||||
-- stage: "on_input" | "pre_action" | "post_action" | ...
|
||||
function sdk.register_stage(stage, handler)
|
||||
print("[lua-plugin] register_stage: " .. tostring(stage))
|
||||
-- scope: nil/"global" (默认) | "own_tools"(仅 before_toolcall/after_toolcall 且工具属于本插件时触发)
|
||||
function sdk.register_stage(stage, handler, scope)
|
||||
print("[lua-plugin] register_stage: " .. tostring(stage) .. " scope=" .. tostring(scope))
|
||||
end
|
||||
|
||||
-- !impl
|
||||
@ -30,6 +31,19 @@ function sdk.register_api(name)
|
||||
print("[lua-plugin] register_api: " .. tostring(name))
|
||||
end
|
||||
|
||||
-- !impl
|
||||
-- def: { no_memory=true/false, cleaner=function(text)->text }
|
||||
-- handler: function(args) -> result
|
||||
function sdk.register_output_channel(name, caps, desc, def, handler)
|
||||
print("[lua-plugin] register_output_channel: " .. tostring(name))
|
||||
end
|
||||
|
||||
-- !impl
|
||||
-- def: { no_memory=true/false, cleaner=function(text)->text }
|
||||
function sdk.register_input_channel(name, def)
|
||||
print("[lua-plugin] register_input_channel: " .. tostring(name))
|
||||
end
|
||||
|
||||
-- !impl
|
||||
function sdk.get_setting(key)
|
||||
return nil
|
||||
@ -55,6 +69,111 @@ function sdk.inject_text_no_memory(source, channel, text)
|
||||
print("[lua-plugin] inject_text_no_memory: " .. tostring(source))
|
||||
end
|
||||
|
||||
-- !impl
|
||||
-- enabled: true/false,崩溃时内核自动拉起
|
||||
function sdk.set_auto_restart(enabled)
|
||||
print("[lua-plugin] set_auto_restart: " .. tostring(enabled))
|
||||
end
|
||||
|
||||
-- ============ graph memory ============
|
||||
-- !impl
|
||||
sdk.memory = {}
|
||||
-- !impl
|
||||
-- query: string, depth: number -> {entities={...}, relations={...}}
|
||||
function sdk.memory.recall(query, depth) return {entities={}, relations={}} end
|
||||
-- !impl
|
||||
-- triples: { {subject=, relation=, object=, [confidence=], [sentence_text=]} } -> err
|
||||
function sdk.memory.commit(triples) return nil end
|
||||
-- !impl
|
||||
function sdk.memory.introspect() return {} end
|
||||
-- !impl
|
||||
function sdk.memory.merge(source, target) return 0 end
|
||||
-- !impl
|
||||
-- criteria: {key=value}, hard: boolean
|
||||
function sdk.memory.purge(criteria, hard) return 0 end
|
||||
|
||||
-- ============ document memory ============
|
||||
-- !impl
|
||||
sdk.doc = {}
|
||||
-- !impl
|
||||
function sdk.doc.query(text, top_k) return {} end
|
||||
-- !impl
|
||||
-- doc: { id=, title=, content= }
|
||||
function sdk.doc.insert(doc) return nil end
|
||||
-- !impl
|
||||
function sdk.doc.remove(id) return nil end
|
||||
-- !impl
|
||||
function sdk.doc.stats() return {} end
|
||||
|
||||
-- ============ knowledge ============
|
||||
-- !impl
|
||||
sdk.knowledge = {}
|
||||
-- !impl
|
||||
function sdk.knowledge.search(query, limit) return {} end
|
||||
-- !impl
|
||||
function sdk.knowledge.add(tag, content) return nil end
|
||||
-- !impl
|
||||
function sdk.knowledge.list() return {} end
|
||||
|
||||
-- ============ text memory ============
|
||||
-- !impl
|
||||
sdk.text_memory = {}
|
||||
-- !impl
|
||||
-- evt: { timestamp=, role=, content=, channel= }
|
||||
function sdk.text_memory.append(evt) return nil end
|
||||
|
||||
-- ============ llm ============
|
||||
-- !impl
|
||||
sdk.llm = {}
|
||||
-- !impl
|
||||
function sdk.llm.list_sources() return {} end
|
||||
-- !impl
|
||||
function sdk.llm.set_source(name) return nil end
|
||||
-- !impl
|
||||
function sdk.llm.current_source() return nil end
|
||||
|
||||
-- ============ social (只读) ============
|
||||
-- !impl
|
||||
sdk.social = {}
|
||||
-- !impl
|
||||
function sdk.social.get_person(name) return {} end
|
||||
-- !impl
|
||||
function sdk.social.get_network(name, depth) return {} end
|
||||
-- !impl
|
||||
function sdk.social.get_trait(name, trait) return {value=nil, found=false} end
|
||||
-- !impl
|
||||
function sdk.social.get_relations(name) return {} end
|
||||
-- !impl
|
||||
function sdk.social.list_persons() return {} end
|
||||
|
||||
-- ============ settings (作用域变体) ============
|
||||
-- !impl
|
||||
sdk.settings = {}
|
||||
-- !impl
|
||||
function sdk.settings.get_core(key) return nil end
|
||||
-- !impl
|
||||
function sdk.settings.set_core(key, value) return nil end
|
||||
-- !impl
|
||||
function sdk.settings.list_core(prefix) return {} end
|
||||
-- !impl
|
||||
function sdk.settings.get_plugin(plugin, key) return nil end
|
||||
-- !impl
|
||||
function sdk.settings.set_plugin(plugin, key, value) return nil end
|
||||
-- !impl
|
||||
function sdk.settings.list_plugin(plugin, prefix) return {} end
|
||||
-- !impl
|
||||
function sdk.settings.list(prefix) return {} end
|
||||
-- !impl
|
||||
-- def: { key=, type=, display_name=, description=, category=, options=, default=,
|
||||
-- min=, max=, step=, required=, secret= }
|
||||
function sdk.settings.register_def(def) return nil end
|
||||
-- !impl
|
||||
function sdk.settings.defs(prefix) return {} end
|
||||
-- !impl
|
||||
function sdk.settings.dump() return {} end
|
||||
-- !impl
|
||||
function sdk.settings.plugins() return {} end
|
||||
|
||||
-- json utils (pure Lua)
|
||||
sdk.json = {}
|
||||
|
||||
|
||||
@ -83,4 +83,5 @@ const (
|
||||
CoreSettingsDefs = 43
|
||||
CoreSettingsDump = 44
|
||||
CoreSettingsPlugins = 45
|
||||
CoreRegisterInputCh = 46
|
||||
)
|
||||
|
||||
@ -650,6 +650,17 @@ func go_core_dispatch(methodID C.int, ctx unsafe.Pointer, s1, s2, s3 *C.char, i1
|
||||
setResult(result, string(b))
|
||||
}
|
||||
return 0
|
||||
|
||||
case 46: // CORE_REGISTER_INPUT_CH
|
||||
chDef := sdk.ChannelDef{}
|
||||
if a2 != "" {
|
||||
var def sdk.ChannelDef
|
||||
if err := json.Unmarshal([]byte(a2), &def); err == nil {
|
||||
chDef = def
|
||||
}
|
||||
}
|
||||
s.RegisterInputChannel(a1, chDef)
|
||||
return 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -55,4 +55,5 @@ const (
|
||||
CoreSettingsDefs = meta.CoreSettingsDefs
|
||||
CoreSettingsDump = meta.CoreSettingsDump
|
||||
CoreSettingsPlugins = meta.CoreSettingsPlugins
|
||||
CoreRegisterInputCh = meta.CoreRegisterInputCh
|
||||
)
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -17,14 +19,28 @@ type toolReg struct {
|
||||
handler *lua.LFunction
|
||||
}
|
||||
|
||||
type outputChReg struct {
|
||||
caps int
|
||||
desc string
|
||||
def sdk.ChannelDef
|
||||
handler *lua.LFunction
|
||||
}
|
||||
|
||||
type stageReg struct {
|
||||
handler *lua.LFunction
|
||||
scope sdk.StageScope
|
||||
}
|
||||
|
||||
// luaPlugin wraps a Lua script as an sdk.Plugin.
|
||||
type luaPlugin struct {
|
||||
name string
|
||||
L *lua.LState
|
||||
tbl *lua.LTable
|
||||
tools map[string]*toolReg
|
||||
stages map[sdk.Stage]*lua.LFunction
|
||||
mu sync.Mutex
|
||||
name string
|
||||
L *lua.LState
|
||||
tbl *lua.LTable
|
||||
tools map[string]*toolReg
|
||||
stages map[sdk.Stage]*stageReg
|
||||
outputChs map[string]*outputChReg
|
||||
inputDefs map[string]sdk.ChannelDef
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newLuaPlugin(luaPath, name string) (*luaPlugin, error) {
|
||||
@ -47,10 +63,12 @@ func newLuaPlugin(luaPath, name string) (*luaPlugin, error) {
|
||||
L.SetTop(0)
|
||||
|
||||
plg := &luaPlugin{
|
||||
name: name,
|
||||
L: L,
|
||||
tools: make(map[string]*toolReg),
|
||||
stages: make(map[sdk.Stage]*lua.LFunction),
|
||||
name: name,
|
||||
L: L,
|
||||
tools: make(map[string]*toolReg),
|
||||
stages: make(map[sdk.Stage]*stageReg),
|
||||
outputChs: make(map[string]*outputChReg),
|
||||
inputDefs: make(map[string]sdk.ChannelDef),
|
||||
}
|
||||
|
||||
// 2) 替换 !impl 函数为 Go stub(暂存 handler,等 Start 时注册到真实 SDK)
|
||||
@ -88,16 +106,7 @@ func replaceSDKStubs(L *lua.LState, t *lua.LTable, plg *luaPlugin) {
|
||||
defTbl := L.CheckTable(2)
|
||||
handler := L.CheckFunction(3)
|
||||
|
||||
goDef := sdk.ToolDef{Name: toolName, Plugin: plg.name}
|
||||
goDef.Description = defTbl.RawGetString("description").String()
|
||||
if params := defTbl.RawGetString("parameters"); params != nil {
|
||||
if pt, ok := params.(*lua.LTable); ok {
|
||||
goDef.Parameters = make(map[string]interface{})
|
||||
pt.ForEach(func(k, v lua.LValue) {
|
||||
goDef.Parameters[k.String()] = luaValueToGo(v)
|
||||
})
|
||||
}
|
||||
}
|
||||
goDef := parseToolDef(L, defTbl, plg, toolName)
|
||||
|
||||
plg.mu.Lock()
|
||||
plg.tools[toolName] = &toolReg{def: goDef, handler: handler}
|
||||
@ -108,8 +117,9 @@ func replaceSDKStubs(L *lua.LState, t *lua.LTable, plg *luaPlugin) {
|
||||
t.RawSetString("register_stage", L.NewFunction(func(L *lua.LState) int {
|
||||
stage := sdk.Stage(L.CheckString(1))
|
||||
handler := L.CheckFunction(2)
|
||||
scope := parseStageScope(L)
|
||||
plg.mu.Lock()
|
||||
plg.stages[stage] = handler
|
||||
plg.stages[stage] = &stageReg{handler: handler, scope: scope}
|
||||
plg.mu.Unlock()
|
||||
return 0
|
||||
}))
|
||||
@ -118,6 +128,33 @@ func replaceSDKStubs(L *lua.LState, t *lua.LTable, plg *luaPlugin) {
|
||||
return 0
|
||||
}))
|
||||
|
||||
t.RawSetString("register_output_channel", L.NewFunction(func(L *lua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
caps := L.CheckInt(2)
|
||||
desc := L.CheckString(3)
|
||||
defTbl := L.CheckTable(4)
|
||||
handler := L.CheckFunction(5)
|
||||
|
||||
chDef := parseChannelDef(L, defTbl, plg)
|
||||
|
||||
plg.mu.Lock()
|
||||
plg.outputChs[name] = &outputChReg{caps: caps, desc: desc, def: chDef, handler: handler}
|
||||
plg.mu.Unlock()
|
||||
return 0
|
||||
}))
|
||||
|
||||
t.RawSetString("register_input_channel", L.NewFunction(func(L *lua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
defTbl := L.CheckTable(2)
|
||||
|
||||
chDef := parseChannelDef(L, defTbl, plg)
|
||||
|
||||
plg.mu.Lock()
|
||||
plg.inputDefs[name] = chDef
|
||||
plg.mu.Unlock()
|
||||
return 0
|
||||
}))
|
||||
|
||||
t.RawSetString("get_setting", L.NewFunction(func(L *lua.LState) int {
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
@ -184,16 +221,7 @@ func replaceSDKReal(L *lua.LState, t *lua.LTable, plg *luaPlugin, s *sdk.PluginS
|
||||
defTbl := L.CheckTable(2)
|
||||
handler := L.CheckFunction(3)
|
||||
|
||||
goDef := sdk.ToolDef{Name: toolName, Plugin: plg.name}
|
||||
goDef.Description = defTbl.RawGetString("description").String()
|
||||
if params := defTbl.RawGetString("parameters"); params != nil {
|
||||
if pt, ok := params.(*lua.LTable); ok {
|
||||
goDef.Parameters = make(map[string]interface{})
|
||||
pt.ForEach(func(k, v lua.LValue) {
|
||||
goDef.Parameters[k.String()] = luaValueToGo(v)
|
||||
})
|
||||
}
|
||||
}
|
||||
goDef := parseToolDef(L, defTbl, plg, toolName)
|
||||
|
||||
h := makeToolHandler(plg, toolName, handler)
|
||||
if err := s.RegisterTool(toolName, goDef, h); err != nil {
|
||||
@ -205,9 +233,10 @@ func replaceSDKReal(L *lua.LState, t *lua.LTable, plg *luaPlugin, s *sdk.PluginS
|
||||
t.RawSetString("register_stage", L.NewFunction(func(L *lua.LState) int {
|
||||
stage := sdk.Stage(L.CheckString(1))
|
||||
handler := L.CheckFunction(2)
|
||||
scope := parseStageScope(L)
|
||||
|
||||
h := makeStageHandler(plg, stage, handler)
|
||||
s.RegisterStage(stage, h)
|
||||
s.RegisterStage(stage, h, scope)
|
||||
return 0
|
||||
}))
|
||||
|
||||
@ -217,6 +246,34 @@ func replaceSDKReal(L *lua.LState, t *lua.LTable, plg *luaPlugin, s *sdk.PluginS
|
||||
return 0
|
||||
}))
|
||||
|
||||
t.RawSetString("register_output_channel", L.NewFunction(func(L *lua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
caps := L.CheckInt(2)
|
||||
desc := L.CheckString(3)
|
||||
defTbl := L.CheckTable(4)
|
||||
handler := L.CheckFunction(5)
|
||||
|
||||
chDef := parseChannelDef(L, defTbl, plg)
|
||||
|
||||
h := makeOutputHandler(plg, handler)
|
||||
if err := s.RegisterOutputChannel(name, caps, desc, chDef, h); err != nil {
|
||||
L.RaiseError("register_output_channel: %v", err)
|
||||
}
|
||||
return 0
|
||||
}))
|
||||
|
||||
t.RawSetString("register_input_channel", L.NewFunction(func(L *lua.LState) int {
|
||||
name := L.CheckString(1)
|
||||
defTbl := L.CheckTable(2)
|
||||
|
||||
chDef := parseChannelDef(L, defTbl, plg)
|
||||
|
||||
if err := s.RegisterInputChannel(name, chDef); err != nil {
|
||||
L.RaiseError("register_input_channel: %v", err)
|
||||
}
|
||||
return 0
|
||||
}))
|
||||
|
||||
t.RawSetString("get_setting", L.NewFunction(func(L *lua.LState) int {
|
||||
key := L.CheckString(1)
|
||||
val, _ := s.Settings().Get(key)
|
||||
@ -242,6 +299,407 @@ func replaceSDKReal(L *lua.LState, t *lua.LTable, plg *luaPlugin, s *sdk.PluginS
|
||||
s.InjectTextNoMemory(L.CheckString(1), L.CheckString(2), L.CheckString(3))
|
||||
return 0
|
||||
}))
|
||||
|
||||
// ---- 数据类 API(与 C ABI 外部插件面完全对齐)----
|
||||
// 约定:结果型返回 (result, err),void 型返回 (nil, err),成功时 err 为 nil。
|
||||
|
||||
subTable := func(name string) *lua.LTable {
|
||||
if v := t.RawGetString(name); v != nil {
|
||||
if st, ok := v.(*lua.LTable); ok {
|
||||
return st
|
||||
}
|
||||
}
|
||||
st := L.NewTable()
|
||||
t.RawSetString(name, st)
|
||||
return st
|
||||
}
|
||||
pushVal := func(val interface{}) int {
|
||||
L.Push(jsonToLuaValue(L, val))
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}
|
||||
// pushList 归一化 nil/空 切片与 map 为 Lua 空表。
|
||||
pushList := func(val interface{}) int {
|
||||
if val == nil {
|
||||
return pushVal([]interface{}{})
|
||||
}
|
||||
v := reflect.ValueOf(val)
|
||||
switch v.Kind() {
|
||||
case reflect.Slice, reflect.Array, reflect.Map:
|
||||
if v.Len() == 0 {
|
||||
return pushVal([]interface{}{})
|
||||
}
|
||||
}
|
||||
return pushVal(val)
|
||||
}
|
||||
pushErr := func(err error) int {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
pushNil := func() int {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}
|
||||
|
||||
// sdk.set_auto_restart(enabled)
|
||||
t.RawSetString("set_auto_restart", L.NewFunction(func(L *lua.LState) int {
|
||||
s.SetAutoRestart(L.CheckBool(1))
|
||||
return 0
|
||||
}))
|
||||
|
||||
// ---- sdk.memory.* (graph memory, 对齐 CORE_MEMORY_*) ----
|
||||
memTbl := subTable("memory")
|
||||
memTbl.RawSetString("recall", L.NewFunction(func(L *lua.LState) int {
|
||||
if m := s.Memory(); m != nil {
|
||||
var query []string
|
||||
switch v := L.Get(1).(type) {
|
||||
case *lua.LTable:
|
||||
v.ForEach(func(_, e lua.LValue) { query = append(query, e.String()) })
|
||||
default:
|
||||
query = []string{L.CheckString(1)}
|
||||
}
|
||||
entities, relations, err := m.Recall(query, L.OptInt(2, 1))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushVal(map[string]interface{}{"entities": entities, "relations": relations})
|
||||
}
|
||||
return pushVal(map[string]interface{}{"entities": []interface{}{}, "relations": []interface{}{}})
|
||||
}))
|
||||
memTbl.RawSetString("commit", L.NewFunction(func(L *lua.LState) int {
|
||||
var triples []sdk.Triple
|
||||
if tbl := L.OptTable(1, nil); tbl != nil {
|
||||
tbl.ForEach(func(_, v lua.LValue) {
|
||||
if t2, ok := v.(*lua.LTable); ok {
|
||||
triples = append(triples, sdk.Triple{
|
||||
Subject: t2.RawGetString("subject").String(),
|
||||
Relation: t2.RawGetString("relation").String(),
|
||||
Object: t2.RawGetString("object").String(),
|
||||
Confidence: float64(lua.LVAsNumber(t2.RawGetString("confidence"))),
|
||||
SubjectType: t2.RawGetString("subject_type").String(),
|
||||
ObjectType: t2.RawGetString("object_type").String(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if m := s.Memory(); m != nil {
|
||||
if err := m.Commit(triples); err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
memTbl.RawSetString("introspect", L.NewFunction(func(L *lua.LState) int {
|
||||
if m := s.Memory(); m != nil {
|
||||
r, err := m.Introspect()
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushVal(r)
|
||||
}
|
||||
return pushVal(map[string]interface{}{})
|
||||
}))
|
||||
memTbl.RawSetString("merge", L.NewFunction(func(L *lua.LState) int {
|
||||
if m := s.Memory(); m != nil {
|
||||
n, err := m.MergeEntities(L.CheckString(1), L.CheckString(2))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushVal(n)
|
||||
}
|
||||
return pushVal(0)
|
||||
}))
|
||||
memTbl.RawSetString("purge", L.NewFunction(func(L *lua.LState) int {
|
||||
mode := "soft"
|
||||
if L.OptBool(2, false) {
|
||||
mode = "hard"
|
||||
}
|
||||
criteria := map[string]string{}
|
||||
if tbl := L.OptTable(1, nil); tbl != nil {
|
||||
tbl.ForEach(func(k, v lua.LValue) {
|
||||
criteria[k.String()] = v.String()
|
||||
})
|
||||
}
|
||||
if m := s.Memory(); m != nil {
|
||||
n, err := m.Purge(criteria, mode)
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushVal(n)
|
||||
}
|
||||
return pushVal(0)
|
||||
}))
|
||||
|
||||
// ---- sdk.doc.* (document memory, 对齐 CORE_DOC_*) ----
|
||||
docTbl := subTable("doc")
|
||||
docTbl.RawSetString("query", L.NewFunction(func(L *lua.LState) int {
|
||||
if dm := s.DocMemory(); dm != nil {
|
||||
return pushList(dm.Query(L.CheckString(1), L.OptInt(2, 5)))
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
docTbl.RawSetString("insert", L.NewFunction(func(L *lua.LState) int {
|
||||
if dm := s.DocMemory(); dm != nil {
|
||||
tbl := L.CheckTable(1)
|
||||
if err := dm.Insert(&sdk.Doc{
|
||||
ID: tbl.RawGetString("id").String(),
|
||||
Title: tbl.RawGetString("title").String(),
|
||||
Content: tbl.RawGetString("content").String(),
|
||||
}); err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
docTbl.RawSetString("remove", L.NewFunction(func(L *lua.LState) int {
|
||||
if dm := s.DocMemory(); dm != nil {
|
||||
dm.Remove(L.CheckString(1))
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
docTbl.RawSetString("stats", L.NewFunction(func(L *lua.LState) int {
|
||||
if dm := s.DocMemory(); dm != nil {
|
||||
return pushVal(dm.Stats())
|
||||
}
|
||||
return pushVal(map[string]interface{}{})
|
||||
}))
|
||||
|
||||
// ---- sdk.knowledge.* (对齐 CORE_KNOWLEDGE_*) ----
|
||||
knTbl := subTable("knowledge")
|
||||
knTbl.RawSetString("search", L.NewFunction(func(L *lua.LState) int {
|
||||
if kn := s.Knowledge(); kn != nil {
|
||||
results, err := kn.Search(L.CheckString(1), L.OptInt(2, 5))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushList(results)
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
knTbl.RawSetString("add", L.NewFunction(func(L *lua.LState) int {
|
||||
if kn := s.Knowledge(); kn != nil {
|
||||
if err := kn.Add(L.CheckString(1), L.CheckString(2)); err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
knTbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int {
|
||||
if kn := s.Knowledge(); kn != nil {
|
||||
list, err := kn.List()
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushList(list)
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
|
||||
// ---- sdk.text_memory.* (对齐 CORE_TEXT_MEMORY_APPEND) ----
|
||||
tmTbl := subTable("text_memory")
|
||||
tmTbl.RawSetString("append", L.NewFunction(func(L *lua.LState) int {
|
||||
if tmem := s.TextMemory(); tmem != nil {
|
||||
tbl := L.CheckTable(1)
|
||||
if err := tmem.Append(sdk.TextEvent{
|
||||
Role: tbl.RawGetString("role").String(),
|
||||
Content: tbl.RawGetString("content").String(),
|
||||
Timestamp: int64(lua.LVAsNumber(tbl.RawGetString("timestamp"))),
|
||||
Channel: tbl.RawGetString("channel").String(),
|
||||
}); err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
|
||||
// ---- sdk.llm.* (对齐 CORE_LLM_*) ----
|
||||
llmTbl := subTable("llm")
|
||||
llmTbl.RawSetString("list_sources", L.NewFunction(func(L *lua.LState) int {
|
||||
if llm := s.LLM(); llm != nil {
|
||||
return pushList(llm.ListSources())
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
llmTbl.RawSetString("set_source", L.NewFunction(func(L *lua.LState) int {
|
||||
if llm := s.LLM(); llm != nil {
|
||||
if err := llm.SetSource(L.CheckString(1)); err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
llmTbl.RawSetString("current_source", L.NewFunction(func(L *lua.LState) int {
|
||||
if llm := s.LLM(); llm != nil {
|
||||
return pushVal(llm.CurrentSource())
|
||||
}
|
||||
return pushVal(nil)
|
||||
}))
|
||||
|
||||
// ---- sdk.social.* (只读,对齐 CORE_SOCIAL_*,当前核心未装配 SocialAPI 时为 nil) ----
|
||||
socTbl := subTable("social")
|
||||
socTbl.RawSetString("get_person", L.NewFunction(func(L *lua.LState) int {
|
||||
if social := s.Social(); social != nil {
|
||||
p, err := social.GetPerson(L.CheckString(1))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushVal(p)
|
||||
}
|
||||
return pushVal(nil)
|
||||
}))
|
||||
socTbl.RawSetString("get_network", L.NewFunction(func(L *lua.LState) int {
|
||||
if social := s.Social(); social != nil {
|
||||
profiles, err := social.GetNetwork(L.CheckString(1), L.OptInt(2, 1))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushList(profiles)
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
socTbl.RawSetString("get_trait", L.NewFunction(func(L *lua.LState) int {
|
||||
if social := s.Social(); social != nil {
|
||||
val, ok := social.GetTrait(L.CheckString(1), L.CheckString(2))
|
||||
return pushVal(map[string]interface{}{"value": val, "found": ok})
|
||||
}
|
||||
return pushVal(map[string]interface{}{"value": nil, "found": false})
|
||||
}))
|
||||
socTbl.RawSetString("get_relations", L.NewFunction(func(L *lua.LState) int {
|
||||
if social := s.Social(); social != nil {
|
||||
rels, err := social.GetRelations(L.CheckString(1))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushList(rels)
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
socTbl.RawSetString("list_persons", L.NewFunction(func(L *lua.LState) int {
|
||||
if social := s.Social(); social != nil {
|
||||
persons, err := social.ListPersons()
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushList(persons)
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
|
||||
// ---- sdk.settings.* (作用域变体,对齐 CORE_SETTINGS_*) ----
|
||||
settTbl := subTable("settings")
|
||||
settTbl.RawSetString("get_core", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
v, err := st.GetCore(L.CheckString(1))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushVal(v)
|
||||
}
|
||||
return pushVal(nil)
|
||||
}))
|
||||
settTbl.RawSetString("set_core", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
if err := st.SetCore(L.CheckString(1), luaValueToGo(L.CheckAny(2))); err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
settTbl.RawSetString("list_core", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
keys, err := st.ListCore(L.OptString(1, ""))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushList(keys)
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
settTbl.RawSetString("get_plugin", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
v, err := st.GetPlugin(L.CheckString(1), L.CheckString(2))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushVal(v)
|
||||
}
|
||||
return pushVal(nil)
|
||||
}))
|
||||
settTbl.RawSetString("set_plugin", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
if err := st.SetPlugin(L.CheckString(1), L.CheckString(2), luaValueToGo(L.CheckAny(3))); err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
settTbl.RawSetString("list_plugin", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
keys, err := st.ListPlugin(L.CheckString(1), L.OptString(2, ""))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushList(keys)
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
settTbl.RawSetString("list", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
keys, err := st.List(L.OptString(1, ""))
|
||||
if err != nil {
|
||||
return pushErr(err)
|
||||
}
|
||||
return pushList(keys)
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
settTbl.RawSetString("register_def", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
tbl := L.CheckTable(1)
|
||||
def := sdk.ConfigDef{
|
||||
Key: tbl.RawGetString("key").String(),
|
||||
Default: luaValueToGo(tbl.RawGetString("default")),
|
||||
Type: tbl.RawGetString("type").String(),
|
||||
DisplayName: tbl.RawGetString("display_name").String(),
|
||||
Description: tbl.RawGetString("description").String(),
|
||||
Category: tbl.RawGetString("category").String(),
|
||||
Min: float64(lua.LVAsNumber(tbl.RawGetString("min"))),
|
||||
Max: float64(lua.LVAsNumber(tbl.RawGetString("max"))),
|
||||
Step: float64(lua.LVAsNumber(tbl.RawGetString("step"))),
|
||||
Required: lua.LVAsBool(tbl.RawGetString("required")),
|
||||
Secret: lua.LVAsBool(tbl.RawGetString("secret")),
|
||||
}
|
||||
if opts := tbl.RawGetString("options"); opts != nil {
|
||||
if ot, ok := opts.(*lua.LTable); ok {
|
||||
ot.ForEach(func(_, v lua.LValue) {
|
||||
def.Options = append(def.Options, v.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
st.RegisterDef(def)
|
||||
}
|
||||
return pushNil()
|
||||
}))
|
||||
settTbl.RawSetString("defs", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
return pushList(st.Defs(L.OptString(1, "")))
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
settTbl.RawSetString("dump", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
return pushVal(st.Dump())
|
||||
}
|
||||
return pushVal(map[string]interface{}{})
|
||||
}))
|
||||
settTbl.RawSetString("plugins", L.NewFunction(func(L *lua.LState) int {
|
||||
if st := s.Settings(); st != nil {
|
||||
return pushList(st.Plugins())
|
||||
}
|
||||
return pushVal([]interface{}{})
|
||||
}))
|
||||
}
|
||||
|
||||
func makeToolHandler(plg *luaPlugin, name string, fn *lua.LFunction) sdk.ToolHandler {
|
||||
@ -265,12 +723,26 @@ func makeStageHandler(plg *luaPlugin, stage sdk.Stage, fn *lua.LFunction) sdk.St
|
||||
plg.mu.Lock()
|
||||
defer plg.mu.Unlock()
|
||||
L := plg.L
|
||||
L.Push(fn)
|
||||
L.Push(goValueToLua(L, map[string]interface{}{
|
||||
ctx := map[string]interface{}{
|
||||
"raw_message": sc.RawMessage,
|
||||
"user_id": sc.UserID,
|
||||
"group_id": sc.GroupID,
|
||||
"phase": string(sc.Phase),
|
||||
}))
|
||||
"llm_text": sc.LLMText,
|
||||
"final_text": sc.FinalText,
|
||||
"no_memory": sc.NoMemory,
|
||||
}
|
||||
if sc.Response != nil {
|
||||
ctx["response"] = *sc.Response
|
||||
}
|
||||
if len(sc.ToolCalls) > 0 {
|
||||
ctx["tool_calls"] = jsonToIface(sc.ToolCalls)
|
||||
}
|
||||
if len(sc.ToolResults) > 0 {
|
||||
ctx["tool_results"] = jsonToIface(sc.ToolResults)
|
||||
}
|
||||
L.Push(fn)
|
||||
L.Push(goValueToLua(L, ctx))
|
||||
if err := L.PCall(1, 0, nil); err != nil {
|
||||
return fmt.Errorf("lua stage %s: %w", stage, err)
|
||||
}
|
||||
@ -278,6 +750,98 @@ func makeStageHandler(plg *luaPlugin, stage sdk.Stage, fn *lua.LFunction) sdk.St
|
||||
}
|
||||
}
|
||||
|
||||
func parseStageScope(L *lua.LState) sdk.StageScope {
|
||||
if L.GetTop() >= 3 && L.ToString(3) == "own_tools" {
|
||||
return sdk.StageScopeOwnTools
|
||||
}
|
||||
return sdk.StageScopeGlobal
|
||||
}
|
||||
|
||||
func parseToolDef(L *lua.LState, defTbl *lua.LTable, plg *luaPlugin, name string) sdk.ToolDef {
|
||||
goDef := sdk.ToolDef{Name: name, Plugin: plg.name}
|
||||
goDef.Description = defTbl.RawGetString("description").String()
|
||||
if v := defTbl.RawGetString("no_memory"); v != nil {
|
||||
goDef.NoMemory = lua.LVAsBool(v)
|
||||
}
|
||||
if v := defTbl.RawGetString("cleaner"); v != nil && v.Type() == lua.LTFunction {
|
||||
goDef.Cleaner = makeLuaCleaner(plg, v.(*lua.LFunction))
|
||||
}
|
||||
if params := defTbl.RawGetString("parameters"); params != nil {
|
||||
if pt, ok := params.(*lua.LTable); ok {
|
||||
goDef.Parameters = make(map[string]interface{})
|
||||
pt.ForEach(func(k, v lua.LValue) {
|
||||
goDef.Parameters[k.String()] = luaValueToGo(v)
|
||||
})
|
||||
}
|
||||
}
|
||||
return goDef
|
||||
}
|
||||
|
||||
func parseChannelDef(L *lua.LState, defTbl *lua.LTable, plg *luaPlugin) sdk.ChannelDef {
|
||||
chDef := sdk.ChannelDef{}
|
||||
if v := defTbl.RawGetString("no_memory"); v != nil {
|
||||
chDef.NoMemory = lua.LVAsBool(v)
|
||||
}
|
||||
if v := defTbl.RawGetString("cleaner"); v != nil && v.Type() == lua.LTFunction {
|
||||
chDef.Cleaner = makeLuaCleaner(plg, v.(*lua.LFunction))
|
||||
}
|
||||
return chDef
|
||||
}
|
||||
|
||||
// jsonToIface 通过 JSON 往返把任意 Go 值转换为 JSON 兼容的 interface{} 树。
|
||||
func jsonToIface(v interface{}) interface{} {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var m interface{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// jsonToLuaValue 通过 JSON 往返把任意 Go 值(struct/slice/map)转换为 Lua 值,
|
||||
// 语义与 C ABI 外部插件跨边界 JSON 序列化一致。
|
||||
func jsonToLuaValue(L *lua.LState, v interface{}) lua.LValue {
|
||||
return goValueToLua(L, jsonToIface(v))
|
||||
}
|
||||
|
||||
func makeLuaCleaner(plg *luaPlugin, fn *lua.LFunction) func(string) string {
|
||||
return func(s string) string {
|
||||
plg.mu.Lock()
|
||||
defer plg.mu.Unlock()
|
||||
L := plg.L
|
||||
L.Push(fn)
|
||||
L.Push(lua.LString(s))
|
||||
if err := L.PCall(1, 1, nil); err != nil {
|
||||
return s
|
||||
}
|
||||
result := L.Get(-1)
|
||||
L.Pop(1)
|
||||
if str, ok := result.(lua.LString); ok {
|
||||
return string(str)
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func makeOutputHandler(plg *luaPlugin, fn *lua.LFunction) sdk.ToolHandler {
|
||||
return func(args map[string]interface{}) (interface{}, error) {
|
||||
plg.mu.Lock()
|
||||
defer plg.mu.Unlock()
|
||||
L := plg.L
|
||||
L.Push(fn)
|
||||
L.Push(goValueToLua(L, args))
|
||||
if err := L.PCall(1, 1, nil); err != nil {
|
||||
return nil, fmt.Errorf("lua output channel: %w", err)
|
||||
}
|
||||
result := L.Get(-1)
|
||||
L.Pop(1)
|
||||
return luaValueToGo(result), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *luaPlugin) Name() string { return p.name }
|
||||
|
||||
func (p *luaPlugin) Start(s *sdk.PluginSDK) error {
|
||||
@ -287,25 +851,40 @@ func (p *luaPlugin) Start(s *sdk.PluginSDK) error {
|
||||
replaceSDKReal(p.L, sdkTable, p, s)
|
||||
}
|
||||
|
||||
// 2) 批量注册加载期已暂存的 tool handler
|
||||
// 2) 批量注册加载期已暂存的 tool / stage / channel handler
|
||||
p.mu.Lock()
|
||||
tools := make(map[string]*toolReg, len(p.tools))
|
||||
for k, v := range p.tools {
|
||||
tools[k] = v
|
||||
}
|
||||
stages := make(map[sdk.Stage]*lua.LFunction, len(p.stages))
|
||||
stages := make(map[sdk.Stage]*stageReg, len(p.stages))
|
||||
for k, v := range p.stages {
|
||||
stages[k] = v
|
||||
}
|
||||
outputChs := make(map[string]*outputChReg, len(p.outputChs))
|
||||
for k, v := range p.outputChs {
|
||||
outputChs[k] = v
|
||||
}
|
||||
inputDefs := make(map[string]sdk.ChannelDef, len(p.inputDefs))
|
||||
for k, v := range p.inputDefs {
|
||||
inputDefs[k] = v
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
for toolName, reg := range tools {
|
||||
h := makeToolHandler(p, toolName, reg.handler)
|
||||
s.RegisterTool(toolName, reg.def, h)
|
||||
}
|
||||
for stage, fn := range stages {
|
||||
h := makeStageHandler(p, stage, fn)
|
||||
s.RegisterStage(stage, h)
|
||||
for stage, reg := range stages {
|
||||
h := makeStageHandler(p, stage, reg.handler)
|
||||
s.RegisterStage(stage, h, reg.scope)
|
||||
}
|
||||
for chName, reg := range outputChs {
|
||||
h := makeOutputHandler(p, reg.handler)
|
||||
s.RegisterOutputChannel(chName, reg.caps, reg.desc, reg.def, h)
|
||||
}
|
||||
for chName, def := range inputDefs {
|
||||
s.RegisterInputChannel(chName, def)
|
||||
}
|
||||
|
||||
// 3) 调用插件的 start(sdk) 回调
|
||||
|
||||
@ -4,6 +4,10 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
func TestTryLoadLua_Basic(t *testing.T) {
|
||||
@ -114,3 +118,408 @@ end)
|
||||
}
|
||||
t.Logf("tool registered during load phase: OK")
|
||||
}
|
||||
|
||||
func TestLuaChannelsAndMemoryDefs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{
|
||||
"name": "chandefs",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua"
|
||||
}`), 0644)
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "main.lua"), []byte(`
|
||||
local plugin = { name = "chandefs" }
|
||||
|
||||
function plugin.start(sdk)
|
||||
sdk.register_tool("mem_tool", {
|
||||
description = "tool with memory defs",
|
||||
parameters = {type = "object", properties = {}},
|
||||
no_memory = true,
|
||||
cleaner = function(text) return "tool:" .. text end
|
||||
}, function(args)
|
||||
return {content = "ok"}
|
||||
end)
|
||||
|
||||
sdk.register_output_channel("out_chan", 1, "output channel", {
|
||||
no_memory = true,
|
||||
cleaner = function(text) return "out:" .. text end
|
||||
}, function(args)
|
||||
return {ok = true}
|
||||
end)
|
||||
|
||||
sdk.register_input_channel("in_chan", {
|
||||
no_memory = false,
|
||||
cleaner = function(text) return "in:" .. text end
|
||||
})
|
||||
end
|
||||
|
||||
function plugin.stop() end
|
||||
return plugin
|
||||
`), 0644)
|
||||
|
||||
plg, err := tryLoadLua(dir, "chandefs", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tryLoadLua failed: %v", err)
|
||||
}
|
||||
|
||||
var gotTool *sdk.ToolDef
|
||||
var gotOutputCh *sdk.ChannelDef
|
||||
var gotOutputHandler sdk.ToolHandler
|
||||
var gotInputDef *sdk.ChannelDef
|
||||
|
||||
regTool := func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
if name == "mem_tool" {
|
||||
gotTool = &def
|
||||
}
|
||||
return nil
|
||||
}
|
||||
regOutput := func(name string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error {
|
||||
if name == "out_chan" {
|
||||
gotOutputCh = &def
|
||||
gotOutputHandler = handler
|
||||
}
|
||||
return nil
|
||||
}
|
||||
regInput := func(name string, def sdk.ChannelDef) error {
|
||||
if name == "in_chan" {
|
||||
gotInputDef = &def
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
s := sdk.New("chandefs", sdk.SDKConfig{RegTool: regTool, RegOutput: regOutput, RegInput: regInput})
|
||||
|
||||
if err := plg.Start(s); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
defer plg.Stop()
|
||||
|
||||
if gotTool == nil {
|
||||
t.Fatal("mem_tool not registered")
|
||||
}
|
||||
|
||||
if !gotTool.NoMemory {
|
||||
t.Error("tool no_memory should be true")
|
||||
}
|
||||
if gotTool.Cleaner == nil {
|
||||
t.Fatal("tool cleaner should not be nil")
|
||||
}
|
||||
if out := gotTool.Cleaner("abc"); out != "tool:abc" {
|
||||
t.Errorf("tool cleaner result = %q, want %q", out, "tool:abc")
|
||||
}
|
||||
|
||||
if gotOutputCh == nil {
|
||||
t.Fatal("output channel not registered")
|
||||
}
|
||||
if !gotOutputCh.NoMemory {
|
||||
t.Error("output channel no_memory should be true")
|
||||
}
|
||||
if gotOutputCh.Cleaner == nil {
|
||||
t.Fatal("output channel cleaner should not be nil")
|
||||
}
|
||||
if out := gotOutputCh.Cleaner("abc"); out != "out:abc" {
|
||||
t.Errorf("output channel cleaner result = %q, want %q", out, "out:abc")
|
||||
}
|
||||
if gotOutputHandler == nil {
|
||||
t.Fatal("output channel handler should not be nil")
|
||||
}
|
||||
res, err := gotOutputHandler(map[string]interface{}{"x": float64(1)})
|
||||
if err != nil {
|
||||
t.Fatalf("output handler error: %v", err)
|
||||
}
|
||||
if m, ok := res.(map[string]interface{}); !ok || m["ok"] != true {
|
||||
t.Errorf("output handler result = %#v, want {ok=true}", res)
|
||||
}
|
||||
|
||||
if gotInputDef == nil {
|
||||
t.Fatal("input channel not registered")
|
||||
}
|
||||
if gotInputDef.NoMemory {
|
||||
t.Error("input channel no_memory should be false")
|
||||
}
|
||||
if gotInputDef.Cleaner == nil {
|
||||
t.Fatal("input channel cleaner should not be nil")
|
||||
}
|
||||
if out := gotInputDef.Cleaner("abc"); out != "in:abc" {
|
||||
t.Errorf("input channel cleaner result = %q, want %q", out, "in:abc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuaChannelsLoadPhaseStash(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"name":"stashch","entry":"main.lua"}`), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "main.lua"), []byte(`
|
||||
-- registered at load phase, before plugin.start(sdk)
|
||||
sdk.register_output_channel("load_out", 2, "desc", {no_memory = true}, function(args) return {r = 1} end)
|
||||
sdk.register_input_channel("load_in", {no_memory = true})
|
||||
return { name = "stashch", start = function(sdk) end, stop = function() end }
|
||||
`), 0644)
|
||||
|
||||
plg, err := tryLoadLua(dir, "stashch", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tryLoadLua failed: %v", err)
|
||||
}
|
||||
lp := plg.(*luaPlugin)
|
||||
|
||||
lp.mu.Lock()
|
||||
outCount := len(lp.outputChs)
|
||||
inCount := len(lp.inputDefs)
|
||||
outReg := lp.outputChs["load_out"]
|
||||
inDef := lp.inputDefs["load_in"]
|
||||
lp.mu.Unlock()
|
||||
|
||||
if outCount != 1 {
|
||||
t.Fatalf("expected 1 stashed output channel, got %d", outCount)
|
||||
}
|
||||
if inCount != 1 {
|
||||
t.Fatalf("expected 1 stashed input channel, got %d", inCount)
|
||||
}
|
||||
if !outReg.def.NoMemory {
|
||||
t.Error("stashed output channel should have NoMemory")
|
||||
}
|
||||
if !inDef.NoMemory {
|
||||
t.Error("stashed input channel should have NoMemory")
|
||||
}
|
||||
|
||||
var gotOut, gotIn bool
|
||||
s := sdk.New("stashch", sdk.SDKConfig{
|
||||
RegOutput: func(name string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error {
|
||||
if name == "load_out" {
|
||||
if !def.NoMemory {
|
||||
t.Error("output channel NoMemory lost through Start")
|
||||
}
|
||||
if handler == nil {
|
||||
t.Error("output channel handler lost through Start")
|
||||
}
|
||||
gotOut = true
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RegInput: func(name string, def sdk.ChannelDef) error {
|
||||
if name == "load_in" {
|
||||
if !def.NoMemory {
|
||||
t.Error("input channel NoMemory lost through Start")
|
||||
}
|
||||
gotIn = true
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if err := plg.Start(s); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
defer plg.Stop()
|
||||
|
||||
if !gotOut || !gotIn {
|
||||
t.Fatalf("channels not registered through Start: out=%v in=%v", gotOut, gotIn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuaAlignedAPIs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"name":"aligned","entry":"main.lua"}`), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "main.lua"), []byte(`
|
||||
local plugin = { name = "aligned" }
|
||||
|
||||
_G.res = {}
|
||||
_G.stage_count = 0
|
||||
_G.stage_ctx = nil
|
||||
|
||||
function plugin.start(sdk)
|
||||
sdk.register_stage("before_toolcall", function(ctx)
|
||||
_G.stage_count = _G.stage_count + 1
|
||||
_G.stage_ctx = ctx
|
||||
end, "own_tools")
|
||||
|
||||
local r, err = sdk.memory.recall("alice", 2)
|
||||
_G.res.recall_ok = (r ~= nil and err == nil)
|
||||
_G.res.recall_entities_type = type(r and r.entities)
|
||||
|
||||
local r2, err2 = sdk.memory.recall({"alice"}, 2)
|
||||
_G.res.recall_array_ok = (r2 ~= nil and err2 == nil)
|
||||
_G.res.recall_array_err = err2
|
||||
|
||||
local _, cerr = sdk.memory.commit({{subject="a", relation="r", object="b"}})
|
||||
_G.res.commit_ok = (cerr == nil)
|
||||
|
||||
local _, ierr = sdk.memory.introspect()
|
||||
_G.res.introspect_ok = (ierr == nil)
|
||||
local _, merr = sdk.memory.merge("a", "b")
|
||||
_G.res.merge_ok = (merr == nil)
|
||||
local _, perr = sdk.memory.purge({}, true)
|
||||
_G.res.purge_ok = (perr == nil)
|
||||
|
||||
local docs = sdk.doc.query("q", 3)
|
||||
_G.res.doc_type = type(docs)
|
||||
local _, dierr = sdk.doc.insert({id="d1", title="t", content="c"})
|
||||
_G.res.doc_insert_ok = (dierr == nil)
|
||||
sdk.doc.remove("d1")
|
||||
_G.res.doc_stats = type(sdk.doc.stats())
|
||||
|
||||
local kres, kerr = sdk.knowledge.search("q")
|
||||
_G.res.kn_search_ok = (kerr == nil and type(kres) == "table")
|
||||
local _, kadderr = sdk.knowledge.add("tag", "content")
|
||||
_G.res.kn_add_ok = (kadderr == nil)
|
||||
local klist = sdk.knowledge.list()
|
||||
_G.res.kn_list_ok = (type(klist) == "table")
|
||||
|
||||
local _, tmerr = sdk.text_memory.append({role="user", content="hello", timestamp=123, channel="c1"})
|
||||
_G.res.tm_ok = (tmerr == nil)
|
||||
|
||||
local srcs = sdk.llm.list_sources()
|
||||
_G.res.llm_list_type = type(srcs)
|
||||
local _, serr = sdk.llm.set_source("default")
|
||||
_G.res.llm_set_ok = (serr == nil)
|
||||
_G.res.llm_cur_type = type(sdk.llm.current_source())
|
||||
|
||||
local p = sdk.social.get_person("alice")
|
||||
_G.res.social_person = p
|
||||
_G.res.social_net_type = type(sdk.social.get_network("alice", 1))
|
||||
local tr = sdk.social.get_trait("alice", "kind")
|
||||
_G.res.social_trait = tr
|
||||
_G.res.social_rels_type = type(sdk.social.get_relations("alice"))
|
||||
_G.res.social_list_type = type(sdk.social.list_persons())
|
||||
|
||||
sdk.set_auto_restart(true)
|
||||
_G.res.auto_restart_called = true
|
||||
|
||||
sdk.settings.set_core("test_lua_key", "lv")
|
||||
_G.res.core_val = sdk.settings.get_core("test_lua_key")
|
||||
local cores = sdk.settings.list_core("test")
|
||||
_G.res.core_list_has = (type(cores) == "table" and #cores > 0)
|
||||
sdk.settings.set_plugin("other", "okey", "oval")
|
||||
_G.res.plugin_val = sdk.settings.get_plugin("other", "okey")
|
||||
_G.res.plugin_list = sdk.settings.list_plugin("other", "")
|
||||
_G.res.sett_list_type = type(sdk.settings.list(""))
|
||||
sdk.settings.register_def({key="def_key", type="string", display_name="DK", default="dv"})
|
||||
_G.res.def_val = sdk.get_setting("def_key")
|
||||
_G.res.defs_type = type(sdk.settings.defs(""))
|
||||
_G.res.dump_type = type(sdk.settings.dump())
|
||||
_G.res.plugins_type = type(sdk.settings.plugins())
|
||||
|
||||
sdk.log("info", "aligned api test done")
|
||||
end
|
||||
|
||||
function plugin.stop() end
|
||||
return plugin
|
||||
`), 0644)
|
||||
|
||||
plg, err := tryLoadLua(dir, "aligned", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("tryLoadLua failed: %v", err)
|
||||
}
|
||||
lp := plg.(*luaPlugin)
|
||||
|
||||
var capturedHandler sdk.StageHandler
|
||||
sett := sdk.NewSettings("aligned", internalConfig.NewConfigRegistry(""))
|
||||
s := sdk.New("aligned", sdk.SDKConfig{
|
||||
Settings: sett,
|
||||
RegStage: func(stage sdk.Stage, handler sdk.StageHandler) {
|
||||
capturedHandler = handler
|
||||
},
|
||||
})
|
||||
|
||||
if err := plg.Start(s); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
defer plg.Stop()
|
||||
|
||||
L := lp.L
|
||||
res, ok := L.GetGlobal("res").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("res global missing")
|
||||
}
|
||||
getBool := func(key string) bool { return lua.LVAsBool(res.RawGetString(key)) }
|
||||
|
||||
if !getBool("recall_ok") {
|
||||
t.Error("memory.recall should succeed (nil-safe)")
|
||||
}
|
||||
if res.RawGetString("recall_entities_type").String() != "table" {
|
||||
t.Error("memory.recall entities should be a table")
|
||||
}
|
||||
if !getBool("recall_array_ok") {
|
||||
t.Error("memory.recall with array query should succeed: " + res.RawGetString("recall_array_err").String())
|
||||
}
|
||||
for _, k := range []string{"commit_ok", "introspect_ok", "merge_ok", "purge_ok", "doc_insert_ok", "kn_search_ok", "kn_add_ok", "kn_list_ok", "tm_ok", "llm_set_ok"} {
|
||||
if !getBool(k) {
|
||||
t.Errorf("%s failed", k)
|
||||
}
|
||||
}
|
||||
if res.RawGetString("doc_type").String() != "table" || res.RawGetString("doc_stats").String() != "table" {
|
||||
t.Error("doc.query/stats should return tables")
|
||||
}
|
||||
if res.RawGetString("llm_list_type").String() != "table" {
|
||||
t.Error("llm list should return tables")
|
||||
}
|
||||
if res.RawGetString("llm_cur_type").String() != "nil" {
|
||||
t.Error("llm.current_source should be nil when LLMAPI not wired")
|
||||
}
|
||||
trTbl, ok := res.RawGetString("social_trait").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("social.get_trait should return a table")
|
||||
}
|
||||
if lua.LVAsBool(trTbl.RawGetString("found")) {
|
||||
t.Error("social.get_trait found should be false when SocialAPI not wired")
|
||||
}
|
||||
if res.RawGetString("social_person").Type() != lua.LTNil {
|
||||
t.Error("social.get_person should be nil when SocialAPI not wired")
|
||||
}
|
||||
if res.RawGetString("social_net_type").String() != "table" ||
|
||||
res.RawGetString("social_rels_type").String() != "table" ||
|
||||
res.RawGetString("social_list_type").String() != "table" {
|
||||
t.Error("social list/network/relations should return tables")
|
||||
}
|
||||
if !getBool("auto_restart_called") {
|
||||
t.Error("set_auto_restart should be callable")
|
||||
}
|
||||
if res.RawGetString("core_val").String() != "lv" {
|
||||
t.Errorf("settings.get_core after set_core = %v, want lv", res.RawGetString("core_val"))
|
||||
}
|
||||
if !getBool("core_list_has") {
|
||||
t.Error("settings.list_core should list set key")
|
||||
}
|
||||
if res.RawGetString("plugin_val").String() != "oval" {
|
||||
t.Errorf("settings.get_plugin = %v, want oval", res.RawGetString("plugin_val"))
|
||||
}
|
||||
if res.RawGetString("def_val").String() != "dv" {
|
||||
t.Errorf("register_def default should be readable via get_setting, got %v", res.RawGetString("def_val"))
|
||||
}
|
||||
for _, k := range []string{"sett_list_type", "defs_type", "dump_type", "plugins_type"} {
|
||||
if res.RawGetString(k).String() != "table" {
|
||||
t.Errorf("%s should be a table", k)
|
||||
}
|
||||
}
|
||||
|
||||
// own_tools scope: 只有本插件工具触发
|
||||
if capturedHandler == nil {
|
||||
t.Fatal("stage handler not registered")
|
||||
}
|
||||
capturedHandler(&sdk.StageContext{
|
||||
RawMessage: "hi",
|
||||
LLMText: "llm text",
|
||||
ToolCalls: []sdk.ToolCall{{Plugin: "aligned", Name: "x"}},
|
||||
})
|
||||
capturedHandler(&sdk.StageContext{
|
||||
ToolCalls: []sdk.ToolCall{{Plugin: "other", Name: "y"}},
|
||||
})
|
||||
if got := int(lua.LVAsNumber(L.GetGlobal("stage_count"))); got != 1 {
|
||||
t.Fatalf("own_tools stage should fire only for own plugin, fired %d", got)
|
||||
}
|
||||
ctxTbl, ok := L.GetGlobal("stage_ctx").(*lua.LTable)
|
||||
if !ok {
|
||||
t.Fatal("stage_ctx global missing")
|
||||
}
|
||||
if ctxTbl.RawGetString("llm_text").String() != "llm text" {
|
||||
t.Errorf("stage ctx llm_text = %v, want 'llm text'", ctxTbl.RawGetString("llm_text"))
|
||||
}
|
||||
if ctxTbl.RawGetString("tool_calls").Type() != lua.LTTable {
|
||||
t.Error("stage ctx tool_calls should be a table")
|
||||
}
|
||||
if ctxTbl.RawGetString("raw_message").String() != "hi" {
|
||||
t.Errorf("stage ctx raw_message = %v, want 'hi'", ctxTbl.RawGetString("raw_message"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user