dsh-lark-bot 0.9.0 → 0.9.2

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/README.md CHANGED
@@ -87,12 +87,14 @@ npx dsh-lark-bot@latest setup --profile dsh-lark
87
87
  ```
88
88
 
89
89
  `setup` 会自动完成:发现本机 dsh → 预批准 pnpm 构建策略 → 执行标准的
90
- `dsh plugin --profile dsh-lark add dsh-lark-bot`,并**默认同时安装「安全网守护」**——系统级
90
+ `dsh plugin --profile dsh-lark add dsh-lark-bot@<版本>`(版本号由当前包固定,避免 pnpm
91
+ 裸名解析到旧版本),并**默认同时安装「安全网守护」**——系统级
91
92
  常驻、dsh 全部下线后仍保留飞书救援入口(核心能力之一,见下文「安全网守护」一节)。
92
93
  一条命令即完成全部安装。
93
94
 
94
95
  `setup` automatically: locates your dsh install → pre-approves pnpm's build policy → runs the
95
- standard `dsh plugin --profile dsh-lark add dsh-lark-bot`, and **also installs the safety-net
96
+ standard `dsh plugin --profile dsh-lark add dsh-lark-bot@<version>` (pinned to the running
97
+ package so pnpm never resolves an outdated bare-name release), and **also installs the safety-net
96
98
  guardian by default** — a system-level resident process that keeps the Feishu rescue entrance
97
99
  alive even when dsh is fully down (one of the core features; see "Safety-net guardian" below).
98
100
  One command installs everything.
@@ -592,6 +594,7 @@ pnpm install
592
594
  pnpm typecheck
593
595
  pnpm test
594
596
  pnpm build
597
+ pnpm check:publish-bundle # 校验 dist 与全部 exports/bin 入口一致(发布前防线)| verifies dist matches every export & the CLI entry (release gate)
595
598
  pnpm ci:local
596
599
  pnpm release:check # ci:local + 上游一致性检查 | ci:local + upstream consistency check
597
600
  pnpm compat:probe # 临时 DSH_HOME 安装锁定版 dsh,跑真实 SDK 握手 | installs pinned dsh into a temp DSH_HOME and runs a real SDK handshake
@@ -623,7 +626,7 @@ pnpm publish:dual:dry-run
623
626
  pnpm publish:dual
624
627
  ```
625
628
 
626
- `scripts/publish-dual-packages.mjs` 从根 `package.json` 生成两份仅 `name` / `bin` 不同的发布清单,避免两份源码漂移。GitHub tag `v*` 会触发 [`release.yml`](.github/workflows/release.yml) 自动发布两个 npm 包并创建 Release。
629
+ `scripts/publish-dual-packages.mjs` 从根 `package.json` 生成两份仅 `name` / `bin` 不同的发布清单,避免两份源码漂移。发布时整目录同步 `dist/`,并在发布前校验 `package.json` 每个 `exports` 子路径与 CLI 入口在产物中都存在——任何缺失(如 v0.9.0 的 `ask` 入口漏拷)都会直接中止发布。GitHub tag `v*` 会触发 [`release.yml`](.github/workflows/release.yml) 自动发布两个 npm 包并创建 Release。
627
630
 
628
631
  `scripts/publish-dual-packages.mjs` generates two publish manifests from the root
629
632
  `package.json`, differing only in `name` / `bin`, so the two copies never drift. A GitHub tag
package/dist/ask.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { Context } from '@deepseek-ai/cordis';
2
+
3
+ /** Cordis plugin name; referenced by the runtime patch row. */
4
+ declare const name = "lark-ask";
5
+ /** Requires the dsh tool registry before registering. */
6
+ declare const inject: string[];
7
+ interface Config {
8
+ /** Localhost callback URL of the running bridge process (/ask). */
9
+ endpoint?: string;
10
+ /** Shared token authorizing the callback. */
11
+ token?: string;
12
+ }
13
+ /** The user may take a while to answer a card; keep the tool well above the
14
+ * default run-timeout so the question itself is not what kills the task. */
15
+ declare const ASK_TOOL_TIMEOUT_MS = 600000;
16
+ /**
17
+ * dsh tool that asks the user a question through a Feishu/Lark card when the
18
+ * agent needs a decision, confirmation, or missing information before
19
+ * proceeding. The bridge runs a localhost-only callback server; this plugin
20
+ * is the runtime side of that channel: it posts the question and blocks until
21
+ * the human answers the card.
22
+ */
23
+ declare function apply(ctx: Context, config?: Config): void;
24
+
25
+ export { ASK_TOOL_TIMEOUT_MS, type Config, apply, inject, name };
package/dist/ask.js ADDED
@@ -0,0 +1,95 @@
1
+ // src/notify/ask-tool.ts
2
+ import { defineTool } from "@deepseek-ai/dsh-tools";
3
+ var name = "lark-ask";
4
+ var inject = ["tools"];
5
+ var ASK_TOOL_TIMEOUT_MS = 6e5;
6
+ function apply(ctx, config = {}) {
7
+ ctx.tools.register(
8
+ defineTool({
9
+ name: "lark_ask_user",
10
+ description: "Ask the user a question through a Feishu/Lark card when you need a decision, confirmation, or missing information before proceeding. The tool blocks until the user answers. Use it sparingly and only for choices or facts only the user can provide; resolve everything discoverable by inspection yourself first.",
11
+ timeoutMs: ASK_TOOL_TIMEOUT_MS,
12
+ parameters: {
13
+ question: {
14
+ type: "string",
15
+ required: true,
16
+ description: "The question to ask the user."
17
+ },
18
+ kind: {
19
+ type: "string",
20
+ enum: ["single", "multi", "text"],
21
+ description: "single = one choice, multi = multiple choices, text = free text. Defaults to single when options are given, otherwise text."
22
+ },
23
+ options: {
24
+ type: "array",
25
+ items: { type: "string" },
26
+ description: "Choices for single / multi questions."
27
+ },
28
+ header: {
29
+ type: "string",
30
+ description: "Optional short heading shown above the question."
31
+ }
32
+ },
33
+ output: {
34
+ schema: {
35
+ type: "object",
36
+ additionalProperties: false,
37
+ properties: {
38
+ answered: { type: "boolean", required: true },
39
+ answer: { type: "json" },
40
+ error: { type: "string" }
41
+ }
42
+ },
43
+ render: (_args, value) => [
44
+ {
45
+ type: "text",
46
+ text: value.answered ? `User answered: ${JSON.stringify(value.answer)}` : `Question failed: ${value.error ?? "no answer"}`
47
+ }
48
+ ]
49
+ },
50
+ async execute(args, exec) {
51
+ const endpoint = config.endpoint ?? process.env.DSH_LARK_ASK_URL;
52
+ const token = config.token ?? process.env.DSH_LARK_NOTIFY_TOKEN;
53
+ if (!endpoint || !token) {
54
+ throw new Error("lark_ask_user is not configured (endpoint/token missing)");
55
+ }
56
+ const sessionId = exec?.agent?.session === void 0 ? void 0 : String(exec.agent.session.id);
57
+ if (!sessionId) {
58
+ throw new Error("lark_ask_user needs an active session to route the question");
59
+ }
60
+ const kind = args.kind ?? (args.options && args.options.length > 0 ? "single" : "text");
61
+ const response = await fetch(endpoint, {
62
+ method: "POST",
63
+ headers: { "content-type": "application/json" },
64
+ body: JSON.stringify({
65
+ token,
66
+ sessionId,
67
+ question: args.question,
68
+ kind,
69
+ ...args.options && args.options.length > 0 ? { options: args.options } : {},
70
+ ...args.header === void 0 ? {} : { header: args.header }
71
+ }),
72
+ ...exec?.signal === void 0 ? {} : { signal: exec.signal }
73
+ });
74
+ const body = await response.json();
75
+ if (!response.ok || body.ok !== true) {
76
+ return {
77
+ answered: false,
78
+ ...body.error === void 0 ? {} : { error: body.error }
79
+ };
80
+ }
81
+ return {
82
+ answered: true,
83
+ ...body.answer === void 0 ? {} : { answer: body.answer ?? null }
84
+ };
85
+ }
86
+ })
87
+ );
88
+ }
89
+ export {
90
+ ASK_TOOL_TIMEOUT_MS,
91
+ apply,
92
+ inject,
93
+ name
94
+ };
95
+ //# sourceMappingURL=ask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/notify/ask-tool.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis';\nimport { defineTool } from '@deepseek-ai/dsh-tools';\n\n/** Cordis plugin name; referenced by the runtime patch row. */\nexport const name = 'lark-ask';\n\n/** Requires the dsh tool registry before registering. */\nexport const inject = ['tools'];\n\nexport interface Config {\n /** Localhost callback URL of the running bridge process (/ask). */\n endpoint?: string;\n /** Shared token authorizing the callback. */\n token?: string;\n}\n\n/** The user may take a while to answer a card; keep the tool well above the\n * default run-timeout so the question itself is not what kills the task. */\nexport const ASK_TOOL_TIMEOUT_MS = 600_000;\n\ninterface AskToolExec {\n agent?: { session?: { id?: unknown } };\n signal?: AbortSignal;\n}\n\n/**\n * dsh tool that asks the user a question through a Feishu/Lark card when the\n * agent needs a decision, confirmation, or missing information before\n * proceeding. The bridge runs a localhost-only callback server; this plugin\n * is the runtime side of that channel: it posts the question and blocks until\n * the human answers the card.\n */\nexport function apply(ctx: Context, config: Config = {}) {\n ctx.tools.register(\n defineTool({\n name: 'lark_ask_user',\n description:\n 'Ask the user a question through a Feishu/Lark card when you need a decision, confirmation, or missing information before proceeding. The tool blocks until the user answers. Use it sparingly and only for choices or facts only the user can provide; resolve everything discoverable by inspection yourself first.',\n timeoutMs: ASK_TOOL_TIMEOUT_MS,\n parameters: {\n question: {\n type: 'string',\n required: true,\n description: 'The question to ask the user.',\n },\n kind: {\n type: 'string',\n enum: ['single', 'multi', 'text'],\n description:\n 'single = one choice, multi = multiple choices, text = free text. Defaults to single when options are given, otherwise text.',\n },\n options: {\n type: 'array',\n items: { type: 'string' },\n description: 'Choices for single / multi questions.',\n },\n header: {\n type: 'string',\n description: 'Optional short heading shown above the question.',\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n answered: { type: 'boolean', required: true },\n answer: { type: 'json' },\n error: { type: 'string' },\n },\n },\n render: (_args, value) => [\n {\n type: 'text',\n text: value.answered\n ? `User answered: ${JSON.stringify(value.answer)}`\n : `Question failed: ${value.error ?? 'no answer'}`,\n },\n ],\n },\n async execute(args, exec: AskToolExec | undefined) {\n // The callback endpoint/token may be configured by the patch row or\n // injected at runtime: the bridge process sets the env vars when its\n // notify server starts, so read them lazily at execute time.\n const endpoint = config.endpoint ?? process.env.DSH_LARK_ASK_URL;\n const token = config.token ?? process.env.DSH_LARK_NOTIFY_TOKEN;\n if (!endpoint || !token) {\n throw new Error('lark_ask_user is not configured (endpoint/token missing)');\n }\n const sessionId =\n exec?.agent?.session === undefined\n ? undefined\n : String(exec.agent.session.id);\n if (!sessionId) {\n throw new Error('lark_ask_user needs an active session to route the question');\n }\n const kind =\n args.kind ?? (args.options && args.options.length > 0 ? 'single' : 'text');\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n token,\n sessionId,\n question: args.question,\n kind,\n ...(args.options && args.options.length > 0 ? { options: args.options } : {}),\n ...(args.header === undefined ? {} : { header: args.header }),\n }),\n ...(exec?.signal === undefined ? {} : { signal: exec.signal }),\n });\n const body = (await response.json()) as {\n ok?: boolean;\n answer?: string | string[] | null;\n error?: string;\n };\n if (!response.ok || body.ok !== true) {\n return {\n answered: false,\n ...(body.error === undefined ? {} : { error: body.error }),\n };\n }\n return {\n answered: true,\n ...(body.answer === undefined ? {} : { answer: body.answer ?? null }),\n };\n },\n }),\n );\n}\n"],"mappings":";AACA,SAAS,kBAAkB;AAGpB,IAAM,OAAO;AAGb,IAAM,SAAS,CAAC,OAAO;AAWvB,IAAM,sBAAsB;AAc5B,SAAS,MAAM,KAAc,SAAiB,CAAC,GAAG;AACvD,MAAI,MAAM;AAAA,IACR,WAAW;AAAA,MACT,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,YAAY;AAAA,QACV,UAAU;AAAA,UACR,MAAM;AAAA,UACN,UAAU;AAAA,UACV,aAAa;AAAA,QACf;AAAA,QACA,MAAM;AAAA,UACJ,MAAM;AAAA,UACN,MAAM,CAAC,UAAU,SAAS,MAAM;AAAA,UAChC,aACE;AAAA,QACJ;AAAA,QACA,SAAS;AAAA,UACP,MAAM;AAAA,UACN,OAAO,EAAE,MAAM,SAAS;AAAA,UACxB,aAAa;AAAA,QACf;AAAA,QACA,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,sBAAsB;AAAA,UACtB,YAAY;AAAA,YACV,UAAU,EAAE,MAAM,WAAW,UAAU,KAAK;AAAA,YAC5C,QAAQ,EAAE,MAAM,OAAO;AAAA,YACvB,OAAO,EAAE,MAAM,SAAS;AAAA,UAC1B;AAAA,QACF;AAAA,QACA,QAAQ,CAAC,OAAO,UAAU;AAAA,UACxB;AAAA,YACE,MAAM;AAAA,YACN,MAAM,MAAM,WACR,kBAAkB,KAAK,UAAU,MAAM,MAAM,CAAC,KAC9C,oBAAoB,MAAM,SAAS,WAAW;AAAA,UACpD;AAAA,QACF;AAAA,MACF;AAAA,MACA,MAAM,QAAQ,MAAM,MAA+B;AAIjD,cAAM,WAAW,OAAO,YAAY,QAAQ,IAAI;AAChD,cAAM,QAAQ,OAAO,SAAS,QAAQ,IAAI;AAC1C,YAAI,CAAC,YAAY,CAAC,OAAO;AACvB,gBAAM,IAAI,MAAM,0DAA0D;AAAA,QAC5E;AACA,cAAM,YACJ,MAAM,OAAO,YAAY,SACrB,SACA,OAAO,KAAK,MAAM,QAAQ,EAAE;AAClC,YAAI,CAAC,WAAW;AACd,gBAAM,IAAI,MAAM,6DAA6D;AAAA,QAC/E;AACA,cAAM,OACJ,KAAK,SAAS,KAAK,WAAW,KAAK,QAAQ,SAAS,IAAI,WAAW;AACrE,cAAM,WAAW,MAAM,MAAM,UAAU;AAAA,UACrC,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU;AAAA,YACnB;AAAA,YACA;AAAA,YACA,UAAU,KAAK;AAAA,YACf;AAAA,YACA,GAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,IAAI,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,YAC3E,GAAI,KAAK,WAAW,SAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO;AAAA,UAC7D,CAAC;AAAA,UACD,GAAI,MAAM,WAAW,SAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO;AAAA,QAC9D,CAAC;AACD,cAAM,OAAQ,MAAM,SAAS,KAAK;AAKlC,YAAI,CAAC,SAAS,MAAM,KAAK,OAAO,MAAM;AACpC,iBAAO;AAAA,YACL,UAAU;AAAA,YACV,GAAI,KAAK,UAAU,SAAY,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM;AAAA,UAC1D;AAAA,QACF;AACA,eAAO;AAAA,UACL,UAAU;AAAA,UACV,GAAI,KAAK,WAAW,SAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,UAAU,KAAK;AAAA,QACrE;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}
package/dist/cli.js CHANGED
@@ -145,7 +145,9 @@ function findOwnPackageRoot(startDir) {
145
145
  try {
146
146
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
147
147
  if (typeof pkg.name === "string" && isOwnPackageName(pkg.name)) {
148
- return { name: pkg.name, root: dir };
148
+ const info = { name: pkg.name, root: dir };
149
+ if (typeof pkg.version === "string") info.version = pkg.version;
150
+ return info;
149
151
  }
150
152
  } catch {
151
153
  }
@@ -246,14 +248,14 @@ function acpPluginInstalled(profileRoot) {
246
248
  }
247
249
  function isAcpProfileReady(profileRoot) {
248
250
  const own = ownPackageInfo();
249
- return existsSync4(join4(profileRoot, "package.json")) && existsSync4(join4(profileRoot, "cordis.yml")) && existsSync4(join4(profileRoot, "cordis.patch.yml")) && acpPluginInstalled(profileRoot) && ownPackageLinked2(profileRoot, own.name);
251
+ return existsSync4(join4(profileRoot, "package.json")) && existsSync4(join4(profileRoot, "cordis.yml")) && existsSync4(join4(profileRoot, "cordis.patch.yml")) && acpPluginInstalled(profileRoot) && ownPackageLinked2(profileRoot, own);
250
252
  }
251
- function ownPackageLinked2(profileRoot, ownName) {
252
- const linkPath = join4(profileRoot, "node_modules", ownName);
253
+ function ownPackageLinked2(profileRoot, own) {
254
+ const linkPath = join4(profileRoot, "node_modules", own.name);
253
255
  try {
254
256
  const real = realpathSync2(linkPath);
255
257
  const pkg = JSON.parse(readFileSync3(join4(real, "package.json"), "utf8"));
256
- return pkg.name === ownName && pkg.dsh?.bundle?.patch !== void 0;
258
+ return pkg.name === own.name && pkg.dsh?.bundle?.patch !== void 0 && real === realpathSync2(own.root);
257
259
  } catch {
258
260
  return false;
259
261
  }
@@ -1352,14 +1354,14 @@ function sdkServerInstalled(profileRoot) {
1352
1354
  }
1353
1355
  function isSdkProfileReady(profileRoot) {
1354
1356
  const own = ownPackageInfo();
1355
- return existsSync3(join3(profileRoot, "package.json")) && existsSync3(join3(profileRoot, "cordis.yml")) && existsSync3(join3(profileRoot, "cordis.patch.yml")) && sdkServerInstalled(profileRoot) && ownPackageLinked(profileRoot, own.name);
1357
+ return existsSync3(join3(profileRoot, "package.json")) && existsSync3(join3(profileRoot, "cordis.yml")) && existsSync3(join3(profileRoot, "cordis.patch.yml")) && sdkServerInstalled(profileRoot) && ownPackageLinked(profileRoot, own);
1356
1358
  }
1357
- function ownPackageLinked(profileRoot, ownName) {
1358
- const linkPath = join3(profileRoot, "node_modules", ownName);
1359
+ function ownPackageLinked(profileRoot, own) {
1360
+ const linkPath = join3(profileRoot, "node_modules", own.name);
1359
1361
  try {
1360
1362
  const real = realpathSync(linkPath);
1361
1363
  const pkg = JSON.parse(readFileSync2(join3(real, "package.json"), "utf8"));
1362
- return pkg.name === ownName && pkg.dsh?.bundle?.patch !== void 0;
1364
+ return pkg.name === own.name && pkg.dsh?.bundle?.patch !== void 0 && real === realpathSync(own.root);
1363
1365
  } catch {
1364
1366
  return false;
1365
1367
  }
@@ -6281,7 +6283,7 @@ async function uninstallGuardian(options) {
6281
6283
  async function runSetup(options = {}) {
6282
6284
  const profile = options.profile ?? "dsh-lark";
6283
6285
  const own = ownPackageInfo();
6284
- const packageSpec = options.packageSpec ?? process.env.DSH_LARK_SETUP_PACKAGE ?? own.name;
6286
+ const packageSpec = options.packageSpec ?? process.env.DSH_LARK_SETUP_PACKAGE ?? (own.version ? `${own.name}@${own.version}` : own.name);
6285
6287
  const dshHome = options.dshHome ?? resolveDshHome(homedir7(), process.env);
6286
6288
  const bin = options.bin ?? discoverDshBin(homedir7(), process.env);
6287
6289
  if (!bin) {