07 - 开发指南¶
环境与常用命令¶
npm install # 安装依赖
npm run build # 生产构建 → dist/aiplugin4.js(头部拼接 header.txt)
npm run build-dev # 开发构建 → dev/aiplugin4.js(带 sourcemap)
npm run smoke # 冒烟:在 Node 中用 seal 桩加载 dist/aiplugin4.js
npm run lint # ESLint 检查 src/**/*.ts
npx tsc --noEmit # 类型检查(直跑 tsconfig,不要加 --strict 覆盖严格选项)
npm run test:unit # 单元测试(esbuild 打包 scripts/unit-test-entry.ts 后运行)
npm run test:integration # 跨仓库联测:配置生成器产出 → 插件 v5/domain 解析(生成器仓库在 ../aiplugin4-config 时运行,否则 SKIP)
node scripts/gen-domain-default-data.mjs # 从旧静态默认值重新生成 src/config/domain/default_data.ts(改默认角色/技能/MCP/知识库后跑)
npm run sim # 上下文成本治理端到端模拟(代码路径级,15 项)
npm run sim:matrix # 消息顺序/时序场景矩阵(90 组顺序×预算 + 时序/状态/边界,76 项)
npm run fuzz # 随机化长跑 fuzz(确定性 PRNG,可 seed 回放)
npm run package:check # 构建 + 同步 sealpack 源 + 包格式与体积校验
验证体系补充:sim / sim:matrix / fuzz 都是代码路径级模拟(真实 Context/Session/handleMessages + 假存储/假模型),能抓回归但不能替代真机——覆盖不到真实厂商 API 的请求体校验、goja 沙箱 storage 行为、QQ 消息形态与多会话并发时序。fuzz 曾抓到「tool 结果与发起它的 assistant 不相邻」的真实 400 bug,适合在改上下文/工具配对后跑。
构建说明(tools/build.js + tools/build-config.js):
- 使用 esbuild,入口
src/index.ts,target 为 es6(生产)/es2020(dev),external: ['csharp','puerts']。 - 产物输出到
dist/aiplugin4.js,并把header.txt(UserScript 注释)拼到文件头部。 - 生产构建不做压缩(minify=false),保持可读、便于用户自行查错。
冒烟测试(scripts/smoke.js):用 seal 桩(Proxy 兜底所有方法)在 Node 中 require('../dist/aiplugin4.js'),可在没有 SealDice 的环境下提前发现加载期 ReferenceError/TypeError。改完核心逻辑建议 npm run build && npm run smoke。
代码组织约定¶
- 核心逻辑放在
src/对应模块目录,src/index.ts只做装配,事件处理统一走src/pipeline.ts。 - 持久化类需要定义
static validKeysMap,并用revive()恢复(见 06-数据存储)。 - 新增消息级能力优先考虑做成"工具"而非写死在命令里,便于 AI 通过函数调用使用。
- 模板文案(如 prompt)放配置模板,不要硬编码在业务逻辑中。
Git 与 Shell 约定¶
- 代码/功能改动不要直接在 main 分支上做修改,先开分支或 PR,review 通过后再合入 main;纯文档改动(如本开发指南)可直接提交 main。
- PR 合入统一使用 squash merge。
- PR 合入 main 后,在本地和远程清理掉对应 PR 分支。
添加一个工具¶
- 在
src/tool/tools/下新建文件(或放入已有分类目录),导出一个registerXxx()函数。 - 创建
new Tool({ type:'function', function:{ name, description, parameters } }),实现solve(ctx, msg, session, args),返回字符串给 AI。 - 可选配置:
sessionType(group/user)、sensitive(敏感标记)。如需调用海豹扩展指令,使用run_ext_command;它会在每次调用时现场构造CmdArgs,不依赖历史指令或.r。 - 在
src/tool/tools/init.ts的registerTools()中导入并调用注册函数(src/tool/tool.ts的Tool.registerTool()统一调用它)。
示例:
// src/tool/tools/tool_say.ts
import Tool from "../tool";
export function registerSay() {
const tool = new Tool({
type: "function",
function: {
name: "say_hi",
description: "打招呼",
parameters: {
type: "object",
properties: {
arg1: { type: "string", description: "说点什么" }
},
required: ["arg1"]
}
}
});
tool.solve = async (_ctx, _msg, _session, args) => {
return `你好,${args.arg1}`;
};
}
// src/tool/tools/init.ts 的 registerTools() 中
import { registerSay } from "./tools/tool_say";
registerSay();
添加一个配置分组¶
- 在
src/config/configs/下新建文件,定义类,实现register()(用seal.ext.registerXxxConfig注册)与get()(读取并返回类型化对象)。 - 在
src/config/config.ts的configMap中登记该分组。 - 使用处直接
Config.<分组>.<字段>读取(带缓存)。
可用注册函数:registerTemplateConfig(多行文本/模板)、registerStringConfig、registerIntConfig、registerFloatConfig、registerBoolConfig、registerOptionConfig(下拉)。
registerTemplateConfig的默认值不能传空数组,至少保留一个元素;仅作占位/示例展示时可用[''](一个空字符串元素)。
添加一个子命令¶
- 在
src/cmd/sub_cmd/新建文件,导出registerCmdXxx(),创建new SubCmd('name')。 - 设置
desc/help/priv,实现solve(scc: SubCmdContext),返回ret。 - 在
src/cmd/root_cmd.ts的SubCmd.register()中调用注册函数。 - 如需要子参数权限,在
priv.args中按命令链嵌套声明。
SubCmdContext 提供:ctx、msg、cmdArgs、epId、uid、gid、sid、session、page、ret。
.ai <子指令> help(无更多参数)是内置保留用法:根命令直接展示该子指令的cmd.help,不校验权限;若子指令需要处理help后的更多参数(如.ai tool help <函数名>),在solve内自行处理。新增子命令务必填写cmd.help。
添加一个智能体¶
- 在
src/agent/agents/新建文件,导出init(),内部用Agent.get(name)获取实例并配置 name/description/instruction/use。 - 在
src/agent/agents/index.ts的initAgents()中调用。 - 若智能体需要独立的模型用途(如
compression、summarization),其请求走Model.getChatModel(this.use)的 chat 类用途默认链:直接取该用途首个可用的 chat 类候选(压缩/总结/judge 等子智能体与 chat 共用同一批文本/视觉候选,落到同一条默认链——各用途首个文本/视觉模型);需要其它模型时用.ai model <用途> <模型>为对应用途绑定。
为其他插件提供 API(globalThis.aiplugin4)¶
插件启动时会把智能体 API 挂载到 globalThis.aiplugin4(幂等,重载不重复覆盖),海豹所有 JS 插件共享同一全局环境,因此其他插件可直接调用,无需依赖本插件的内部实现。调用前建议先判空,兼容本插件未安装的情况。
暴露的方法:
| 成员 | 说明 |
|---|---|
name / version |
插件名与版本号 |
Agent |
Agent 类,可自行 get/save 智能体 |
getAgent(name?) |
获取智能体实例,name 缺省为默认智能体 * |
getSession(agentName, sessionId) |
获取指定智能体的会话 |
chat(prompt, agentName?) |
单轮对话:不经过会话上下文与工具,直接返回模型回复文本 |
chatMessages(messages, agentName?) |
对话:直接发送 OpenAI 风格 messages 数组到对话模型,返回回复文本 |
imageToText(source, prompt?) |
识图:对图片 id 或 http(s) 图片地址调用图片识别模型,返回描述文本 |
embed(text) |
嵌入:调用嵌入模型返回文本向量(未配置嵌入模型时返回空数组) |
run(ctx, msg, options?) |
在当前聊天中触发一次完整对话编排(上下文/工具/回复发送),等价于该会话收到一次触发 |
registerTool(info, options?) |
注册一个工具,供 AI 通过函数调用/提示词工程使用;同名内置工具不可覆盖,成功返回 true |
run 的 options:agentName(智能体名,缺省 *)、reason(触发原因,缺省「外部插件调用」)、toolChoice(强制工具选择策略,缺省交给模型)。registerTool 的 options:sensitive(敏感工具)、sessionType(any/group/user)、callBack(是否回调对话:工具结果写回上下文并继续生成,缺省 true;false 时工具静默执行、结果不回调)、solve(执行函数,返回给 AI 的文本)。
示例(其他插件内):
const api = globalThis.aiplugin4;
if (!api) return; // 本插件未安装
// 单轮对话,直接拿回复文本
const reply = await api.chat('用一句话介绍你自己');
// 对话:构建 messages 数组直接调用模型
const reply2 = await api.chatMessages([
{ role: 'system', content: '你是翻译助手' },
{ role: 'user', content: '翻译:hello' }
]);
// 识图:传图片 id 或 URL,返回描述文本
const desc = await api.imageToText('https://example.com/a.png', '画面里有什么?');
// 嵌入:返回文本向量
const vec = await api.embed('要检索的文本');
// 指定智能体(如 KP 插件驱动 kp_agent)
const hint = await api.chat('根据当前剧情给主持人的私聊提示', 'kp_agent');
// 完整编排:在收到消息的会话中触发 AI 回复(走上下文/工具/分段发送)
await api.run(ctx, msg, { agentName: 'kp_agent', reason: 'KP插件调用' });
// 底层用法:直接拿 Agent 实例
const agent = api.getAgent('*');
const text = await agent.chat('你好');
// 注册工具:供 AI 函数调用/提示词工程使用
api.registerTool({
type: 'function',
function: {
name: 'my_tool',
description: '示例工具:把传入的 text 原样回显',
parameters: {
type: 'object',
properties: {
text: { type: 'string', description: '要回显的内容' }
},
required: ['text']
}
}
}, {
solve: async (_ctx, _msg, _session, args) => `回显: ${args.text}`
});
注意:chat 是直连模型的无状态单轮对话;需要多轮上下文、工具调用或主动回复聊天窗口时使用 run。run 与插件自身触发一致,受请求并发限制与配额(令牌桶)约束。
仓库内附带可直接加载的完整示例插件 examples/use-aiplugin4.js(只注册一个 .apitest 命令,子命令 status/chat/agent/run/tool 覆盖智能体调用与 registerTool 注册工具),可复制到海豹中加载体验。注意示例插件的扩展必须调用 seal.ext.register(ext) 注册,否则指令不会进入海豹指令表。
修改 prompt 模板¶
- 模板在
src/prompt/templates.ts中以内置常量定义(不再注册为配置项,WebUI 无默认值可还原;改动模板后需重新构建)。 - system prompt 的分节组装逻辑在
src/prompt/builder.ts,新增节时需要同步修改模板骨架与构建函数。 - 记忆/知识库/工具段的开关在
src/memory/manager.ts与src/config/configs/memory.ts。
开发完成检查清单¶
每个功能/改动开发完成并验证后,发布前必须同步以下三处,否则发版说明与用户文档会与代码脱节:
src/changelog.ts:在changelog的最新版本条目中补充本次改动(格式- 描述),发布工作流会读取它生成 GitHub Release 正文。README.md:同步配置手册/命令手册/可用工具函数等对应章节,保持与代码一致。- 知识库
docs/:涉及命令/配置/工具的改动需同步对应文档(常用:05-命令与配置.md、04-工具系统.md、03-核心模块详解.md、09-注意事项与常见问题.md)。 - 配置项更改(新增/删除/修改默认值/弃用)必须在
src/changelog.ts发版日志中注明,用「配置项更改」小节列出受影响配置项;涉及模板等默认值变更时,还需在「重要」小节提醒用户恢复默认。
发布流程¶
.github/workflows/:
build-check.yml:main 分支 push / PR 时执行 lint、tsc strict、构建并校验 sealpack(npm run package:check)、冒烟测试。release.yml:推送v*标签时自动发版——verify(校验标签与VERSION/changelog.ts一致)→node scripts/build-release.js打包(本体 JS + 本体豹包 + 完整豹包)→ 用SEALPACK_TOKEN分别发布两个豹包到 SealRepo → 从src/changelog.ts提取版本日志作为 GitHub Release 正文,附带两个豹包与dist/aiplugin4.js。
打包源 sealpack/:scripts/main.js 与 info.toml 版本由 scripts/prepare-sealpack.js 自动同步;sealpack-full/ 为完整包生成目录(复制本体 + 下载依赖插件,已 gitignore)。完整包依赖插件在 scripts/deps.cjs 的 dependencies 中配置(url 为依赖插件 JS 的 raw 地址,可选 filename),当前只配置一个依赖:ob11 网络连接依赖。4.24.0 起生成音频(tts)/ 生成图片(tti)已并入主插件——语音合成与文生图走「模型」页 api连接 + 模型规则的 tts / image-generation 用途(src/config/static_config/media_presets.ts 供应商内置预设 + src/model/media_template.ts 预设合成与占位符引擎 + src/model/media.ts 请求编排 + src/media/task_queue.ts 异步任务队列),不再读取 globalThis.tts / globalThis.tti / globalThis.aiDrawing。包图标放 sealpack/assets/icon.png;SealRepo Token 放 GitHub Actions secrets(SEALPACK_TOKEN)。
配套后端服务已迁移到独立仓库 aiplugin4-backends,由 launcher.py 一键管理(list/setup/start/stop/status/port/package/webui),Windows/Linux 通用;默认不启动任何后端,首次启动某后端时才自动安装其环境(独立 venv),由该仓库自身发版流程打包发布。详见 08-相关后端项目。
发版前记得同步:src/config/static_config/meta.ts 的 VERSION、header.txt 的 @version、package.json 与 package-lock.json 的 version、src/changelog.ts 的 changelog(且需包含新版本条目);sealpack/info.toml 的版本由 scripts/prepare-sealpack.js 自动同步,发版提交也会一并保持一致。
开发小贴士¶
- 日志统一走
Logger(脱敏/截断),不要直接console.log明文密钥。 - 涉及网络请求统一走
withTimeout+fetchData/requestModel,避免请求卡死。 - 修改会持久化的类字段后,检查其
validKeysMap是否需要同步更新。 - 新增工具名不要与已有工具重复;敏感操作(发消息/禁言/改名)记得置
sensitive = true。