dsh-agent-toolkit 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -12,6 +12,7 @@ import { SessionId } from "@deepseek-ai/dsh-session";
12
12
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
13
13
  import * as lark from "@larksuiteoapi/node-sdk";
14
14
  import { randomBytes, randomUUID } from "node:crypto";
15
+ import { bindScopeParent, createScope, scopeOf } from "@deepseek-ai/dsh-scope";
15
16
  import { setupUsage } from "@dsh-agent-toolkit/token-usage";
16
17
  //#region src/agents/store.ts
17
18
  /** agents 注册表存储域声明:记录 schema + domain 布局的单一来源。 */
@@ -1668,18 +1669,15 @@ function setupCreateAgentCommand(ctx, deps) {
1668
1669
  //#region src/channels/agent-setup.ts
1669
1670
  /** bot 会话角色 persona 的 scoped 段名:与全局 persona 层同名,scoped 注册即 shadow 覆盖主 Agent persona。 */
1670
1671
  const TOOLKIT_PERSONA_SECTION = "prompt-stack:persona";
1671
- /** 默认加载器:复刻 preset mount 的模块解析——loader 的 unwrapExports 取 `default ?? 命名导出模块`。 */
1672
- async function loadToolModule(specifier) {
1673
- const mod = await import(specifier);
1674
- return mod.default ?? mod;
1675
- }
1676
1672
  /**
1677
- * 组合 agent 作用域:先 scoped 挂载基础工具行(persona/instructions/shell/fs/fs-search,
1678
- * 与原生 UI 会话的 standard preset 同源),再叠 bot 的 persona 与 tools 白名单。
1679
- * 顺序敏感:restrict 必须在工具行挂载之后,否则白名单命中不到挂载带入的工具名。
1673
+ * 组合 agent 作用域:先把 agent scope 父链挂到基础工具行 standing scope(工具成为
1674
+ * 继承面),再叠 bot 的 persona 与 tools 白名单。
1675
+ * 顺序敏感:restrict 必须在 join 之后——宿主 tools.restrict() 只校验并过滤继承面
1676
+ * (global + 祖先 scope 层),own 层(agentCtx 直接挂载)的名字既不可 restrict 也
1677
+ * 不受 restrict 影响,直接挂进 agentCtx 的白名单会抛 unknown global tools。
1680
1678
  */
1681
- async function setupAgentScope(agentCtx, hooks, loadTool = loadToolModule) {
1682
- for (const tool of BASIC_TOOLS) await agentCtx.plugin(await loadTool(tool.id), tool.config);
1679
+ async function setupAgentScope(agentCtx, hooks, toolsScope) {
1680
+ await toolsScope.join(agentCtx);
1683
1681
  if (hooks.persona !== void 0) agentCtx.systemPrompt.section({
1684
1682
  name: TOOLKIT_PERSONA_SECTION,
1685
1683
  order: 10,
@@ -2266,20 +2264,23 @@ const feishuChannel = {
2266
2264
  appSecret: bot.secret
2267
2265
  }));
2268
2266
  const dedup = new MessageDedup();
2269
- const dispatcher = new lark.EventDispatcher({}).register({ "im.message.receive_v1": async (data) => {
2270
- const parsed = parseMessageEvent(data);
2271
- if (parsed === null || !dedup.check(parsed.messageId)) return;
2272
- const reply = new FeishuReplyHandle(api, parsed.chatId, tunables, log);
2273
- io.onMessage({
2274
- botId: bot.record.id,
2275
- chatId: parsed.chatId,
2276
- userId: parsed.userId,
2277
- messageId: parsed.messageId,
2278
- text: parsed.text,
2279
- reply,
2280
- ackProcessing: makeAck(api, parsed.messageId, tunables.processingReactionEmoji)
2281
- });
2282
- } });
2267
+ const dispatcher = new lark.EventDispatcher({}).register({
2268
+ "im.message.message_read_v1": async () => void 0,
2269
+ "im.message.receive_v1": async (data) => {
2270
+ const parsed = parseMessageEvent(data);
2271
+ if (parsed === null || !dedup.check(parsed.messageId)) return;
2272
+ const reply = new FeishuReplyHandle(api, parsed.chatId, tunables, log);
2273
+ io.onMessage({
2274
+ botId: bot.record.id,
2275
+ chatId: parsed.chatId,
2276
+ userId: parsed.userId,
2277
+ messageId: parsed.messageId,
2278
+ text: parsed.text,
2279
+ reply,
2280
+ ackProcessing: makeAck(api, parsed.messageId, tunables.processingReactionEmoji)
2281
+ });
2282
+ }
2283
+ });
2283
2284
  const ws = new lark.WSClient({
2284
2285
  appId,
2285
2286
  appSecret: bot.secret,
@@ -2342,66 +2343,6 @@ const projectBotDomain = defineDomain({
2342
2343
  function bindingKey(botId, chatId) {
2343
2344
  return `${botId}:${chatId}`;
2344
2345
  }
2345
- //#endregion
2346
- //#region src/channels/inbound.ts
2347
- /** 入站:指令分流 → 路由 → 单 in-flight 准入 → 表情回复 → followup 投递。 */
2348
- var Inbound = class {
2349
- deps;
2350
- constructor(deps) {
2351
- this.deps = deps;
2352
- }
2353
- onMessage(msg) {
2354
- this.handle(msg).catch(async (error) => {
2355
- this.deps.onError(`[project-bot] 入站处理失败:${error instanceof Error ? error.message : String(error)}`);
2356
- await msg.reply.notice("处理失败,请稍后再试").catch(() => void 0);
2357
- });
2358
- }
2359
- async handle(msg) {
2360
- const bot = this.deps.bots.get(msg.botId);
2361
- if (bot === void 0) return;
2362
- const directive = parseDirective(msg.text);
2363
- if (directive === "new") {
2364
- await this.deps.router.reset(bot, msg.chatId, msg.reply);
2365
- await msg.reply.notice("已开启新会话");
2366
- return;
2367
- }
2368
- if (directive === "stop") {
2369
- const rt = this.deps.router.lookup(bot.id, msg.chatId);
2370
- if (rt?.inflight !== void 0) {
2371
- rt.agent.cancel();
2372
- await msg.reply.notice("已请求停止当前任务");
2373
- } else await msg.reply.notice("当前没有进行中的任务");
2374
- return;
2375
- }
2376
- if (directive === "status") {
2377
- const rt = this.deps.router.lookup(bot.id, msg.chatId);
2378
- await msg.reply.notice(rt === void 0 ? `项目:${bot.project}\n会话:未创建(发送消息即创建)` : `项目:${bot.project}\n会话:${rt.sessionId}\n状态:${rt.inflight !== void 0 ? "处理中" : "空闲"}`);
2379
- return;
2380
- }
2381
- const rt = await this.deps.router.ensure(bot, msg.chatId, msg.reply);
2382
- if (rt.inflight !== void 0) {
2383
- await msg.reply.notice("上一条还在处理中,请稍候(或发送 /stop 取消)");
2384
- return;
2385
- }
2386
- rt.inflight = { ack: void 0 };
2387
- rt.inflight.ack = await msg.ackProcessing().catch(() => void 0) ?? void 0;
2388
- const message = createUserMessage({
2389
- content: [{
2390
- type: "text",
2391
- text: msg.text
2392
- }],
2393
- source: { kind: "user" }
2394
- });
2395
- try {
2396
- rt.agent.followup(message);
2397
- } catch (error) {
2398
- const ack = rt.inflight.ack;
2399
- rt.inflight = void 0;
2400
- await ack?.();
2401
- throw error;
2402
- }
2403
- }
2404
- };
2405
2346
  /** 向段序列追加内容:与尾段同类则合并,异类开新段。 */
2406
2347
  function appendToSegments(segments, kind, text) {
2407
2348
  const tail = segments[segments.length - 1];
@@ -2519,6 +2460,67 @@ var Outbound = class {
2519
2460
  }
2520
2461
  };
2521
2462
  //#endregion
2463
+ //#region src/channels/inbound.ts
2464
+ /** 入站:指令分流 → 路由 → 单 in-flight 准入 → 表情回复 → followup 投递。 */
2465
+ var Inbound = class {
2466
+ deps;
2467
+ constructor(deps) {
2468
+ this.deps = deps;
2469
+ }
2470
+ onMessage(msg) {
2471
+ this.handle(msg).catch(async (error) => {
2472
+ const detail = truncateDetail(error instanceof Error ? error.message : String(error), this.deps.maxErrorDetailChars);
2473
+ this.deps.onError(`[project-bot] 入站处理失败:${detail}`);
2474
+ await msg.reply.notice(`处理失败:${detail}`).catch(() => void 0);
2475
+ });
2476
+ }
2477
+ async handle(msg) {
2478
+ const bot = this.deps.bots.get(msg.botId);
2479
+ if (bot === void 0) return;
2480
+ const directive = parseDirective(msg.text);
2481
+ if (directive === "new") {
2482
+ await this.deps.router.reset(bot, msg.chatId, msg.reply);
2483
+ await msg.reply.notice("已开启新会话");
2484
+ return;
2485
+ }
2486
+ if (directive === "stop") {
2487
+ const rt = this.deps.router.lookup(bot.id, msg.chatId);
2488
+ if (rt?.inflight !== void 0) {
2489
+ rt.agent.cancel();
2490
+ await msg.reply.notice("已请求停止当前任务");
2491
+ } else await msg.reply.notice("当前没有进行中的任务");
2492
+ return;
2493
+ }
2494
+ if (directive === "status") {
2495
+ const rt = this.deps.router.lookup(bot.id, msg.chatId);
2496
+ await msg.reply.notice(rt === void 0 ? `项目:${bot.project}\n会话:未创建(发送消息即创建)` : `项目:${bot.project}\n会话:${rt.sessionId}\n状态:${rt.inflight !== void 0 ? "处理中" : "空闲"}`);
2497
+ return;
2498
+ }
2499
+ const rt = await this.deps.router.ensure(bot, msg.chatId, msg.reply);
2500
+ if (rt.inflight !== void 0) {
2501
+ await msg.reply.notice("上一条还在处理中,请稍候(或发送 /stop 取消)");
2502
+ return;
2503
+ }
2504
+ rt.inflight = { ack: void 0 };
2505
+ rt.inflight.ack = await msg.ackProcessing().catch(() => void 0) ?? void 0;
2506
+ const message = createUserMessage({
2507
+ content: [{
2508
+ type: "text",
2509
+ text: msg.text
2510
+ }],
2511
+ source: { kind: "user" }
2512
+ });
2513
+ try {
2514
+ rt.agent.followup(message);
2515
+ } catch (error) {
2516
+ const ack = rt.inflight.ack;
2517
+ rt.inflight = void 0;
2518
+ await ack?.();
2519
+ throw error;
2520
+ }
2521
+ }
2522
+ };
2523
+ //#endregion
2522
2524
  //#region src/channels/ports.ts
2523
2525
  /** 从 bot 记录提取创作期注入(主 Agent 绑定形态)。 */
2524
2526
  function hooksOf(bot) {
@@ -2659,6 +2661,7 @@ var BotRuntime = class {
2659
2661
  this.inbound = new Inbound({
2660
2662
  router: this.router,
2661
2663
  bots: deps.bots,
2664
+ maxErrorDetailChars: deps.maxErrorDetailChars,
2662
2665
  onError: (m) => deps.log.warn(m)
2663
2666
  });
2664
2667
  this.outbound = new Outbound(this.sessions, (m) => deps.log.warn(m), deps.maxErrorDetailChars);
@@ -2742,6 +2745,55 @@ var BotRuntime = class {
2742
2745
  }
2743
2746
  };
2744
2747
  //#endregion
2748
+ //#region src/channels/tool-scope.ts
2749
+ /** 默认加载器:复刻 preset mount 的模块解析——loader 的 unwrapExports 取 `default ?? 命名导出模块`。 */
2750
+ async function loadToolModule(specifier) {
2751
+ const mod = await import(specifier);
2752
+ return mod.default ?? mod;
2753
+ }
2754
+ /** 建一个 bot 会话共用的工具行 standing scope(所有 bot 会话挂同一套 BASIC_TOOLS)。 */
2755
+ function createToolsScope(ctx, loadTool = loadToolModule) {
2756
+ let scope;
2757
+ let mount;
2758
+ let generation = 0;
2759
+ function ensure() {
2760
+ mount ??= (async () => {
2761
+ const key = {
2762
+ origin: "dsh-agent-toolkit",
2763
+ kind: "bots-tools",
2764
+ gen: ++generation
2765
+ };
2766
+ const created = createScope(ctx, key);
2767
+ scope = created;
2768
+ try {
2769
+ for (const tool of BASIC_TOOLS) await created.ctx.plugin(await loadTool(tool.id), tool.config);
2770
+ } catch (error) {
2771
+ if (scope === created) scope = void 0;
2772
+ mount = void 0;
2773
+ await created.dispose().catch(() => void 0);
2774
+ throw error;
2775
+ }
2776
+ return key;
2777
+ })();
2778
+ return mount;
2779
+ }
2780
+ return {
2781
+ async join(agentCtx) {
2782
+ const agentKey = scopeOf(agentCtx);
2783
+ if (agentKey === void 0) throw new Error("tools-scope: refusing to join an unscoped agent context; per-agent composition requires a scope key");
2784
+ const standing = await ensure();
2785
+ bindScopeParent(agentKey, standing);
2786
+ return standing;
2787
+ },
2788
+ async dispose() {
2789
+ mount = void 0;
2790
+ const current = scope;
2791
+ scope = void 0;
2792
+ await current?.dispose();
2793
+ }
2794
+ };
2795
+ }
2796
+ //#endregion
2745
2797
  //#region src/bots/api.ts
2746
2798
  /** 浏览器半 RPC:单前缀路由 /dsh-agent-toolkit/api/bots + 内部路径分发。 */
2747
2799
  const MAX_BODY_BYTES = 65536;
@@ -2993,6 +3045,18 @@ function createApiHandler(deps) {
2993
3045
  //#endregion
2994
3046
  //#region src/bots/register-app.ts
2995
3047
  /** 扫码一键创建飞书应用:lark.registerApp(OAuth 2.0 Device Authorization Grant)的状态机封装。 */
3048
+ /** 扫码创建应用时申请的权限/事件(流式卡片 + 收发消息 + 表情 + 通讯录基础信息)。
3049
+ * 故意不加 as const:readonly 元组不可赋值给 SDK AppAddons 的 mutable string[],
3050
+ * 否则 bots/index.ts 的 lark.registerApp(options) 透传会 typecheck 失败。 */
3051
+ const FEISHU_REGISTER_APP_ADDONS = {
3052
+ scopes: { tenant: [
3053
+ "im:message",
3054
+ "im:message:send_as_bot",
3055
+ "cardkit:card:write",
3056
+ "contact:user.base:readonly"
3057
+ ] },
3058
+ events: { items: { tenant: ["im.message.receive_v1"] } }
3059
+ };
2996
3060
  var RegisterAppService = class {
2997
3061
  deps;
2998
3062
  sessions = /* @__PURE__ */ new Map();
@@ -3013,6 +3077,7 @@ var RegisterAppService = class {
3013
3077
  this.sessions.set(id, entry);
3014
3078
  this.deps.registerApp({
3015
3079
  signal: controller.signal,
3080
+ addons: FEISHU_REGISTER_APP_ADDONS,
3016
3081
  onQRCodeReady: (info) => {
3017
3082
  entry.state = {
3018
3083
  status: "pending",
@@ -3071,21 +3136,22 @@ function setupBots(ctx, config, deps) {
3071
3136
  await ctx.credentials.set(credentialRef(ref), secret);
3072
3137
  return ref;
3073
3138
  };
3074
- /** 创作期注入已迁至 agent-setup.ts(基础工具 scoped 挂载 + persona/tools),preset 机制整体移除。 */
3139
+ /** 创作期注入已迁至 agent-setup.ts + tool-scope.ts(基础工具行 standing scope 挂载 + persona/tools),preset 机制整体移除。 */
3140
+ const toolsScope = createToolsScope(ctx);
3075
3141
  const agentsPort = {
3076
3142
  async create(input) {
3077
3143
  return adaptAgent(await ctx.agents.create({
3078
3144
  sessionId: SessionId(input.sessionId),
3079
3145
  meta: { cwd: input.cwd },
3080
3146
  ...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
3081
- setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks)
3147
+ setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks, toolsScope)
3082
3148
  }));
3083
3149
  },
3084
3150
  async resume(input) {
3085
3151
  return adaptAgent(await ctx.agents.resume({
3086
3152
  resumeSessionId: SessionId(input.sessionId),
3087
3153
  ...input.agentOptions !== void 0 ? { agentOptions: input.agentOptions } : {},
3088
- setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks)
3154
+ setup: (agentCtx) => setupAgentScope(agentCtx, input.hooks, toolsScope)
3089
3155
  }));
3090
3156
  }
3091
3157
  };
@@ -3189,6 +3255,9 @@ function setupBots(ctx, config, deps) {
3189
3255
  });
3190
3256
  return () => dispose();
3191
3257
  });
3258
+ ctx.effect(() => async () => {
3259
+ await toolsScope.dispose();
3260
+ });
3192
3261
  ctx.effect(() => async () => {
3193
3262
  registerAppService.dispose();
3194
3263
  });