@jesonliu/lark-claudecode-bridge 1.3.2 → 1.3.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.
@@ -1,8 +1,10 @@
1
1
  export declare const FEISHU_NOTIFY_SOP_PROMPT = "\u3010\u98DE\u4E66\u63A8\u9001\u89C4\u8303\uFF08lcb-notify \u4E09\u4EF6\u5957\uFF1Asend_text / send_image / send_file\uFF09\u3011\n\n\u63A8\u9001\u524D\u5148\u5206\u7C7B\u2014\u2014\u53EA\u63A8\u300C\u7528\u6237\u786E\u5B9E\u9700\u8981\u67E5\u770B\u300D\u7684\u5185\u5BB9\uFF1A\n\n\u3010\u5E94\u8BE5\u63A8\u9001\u3011\n- \u7528\u6237\u660E\u786E\u8981\u6C42\u67E5\u770B\u7684\u5185\u5BB9\uFF08\"\u53D1\u6211\u770B\u770B\"\u3001\"\u7ED9\u6211\u6587\u6848\"\uFF09\n- \u9636\u6BB5\u6027\u6210\u679C\u5B9A\u7A3F\uFF1A\u6587\u6848\u7EC8\u7A3F\u3001\u65B9\u6848\u6210\u7A3F\u3001\u6570\u636E\u8868\u683C\u3001\u7ED3\u8BBA\u6E05\u5355\n- \u9700\u8981\u7528\u6237\u786E\u8BA4/\u51B3\u7B56\u7684\u4E2D\u95F4\u4EA7\u7269\uFF1A\u4E8C\u9009\u4E00\u65B9\u6848\u3001\u547D\u540D/\u53C2\u6570\u786E\u8BA4\n- \u5173\u952E\u98CE\u9669\u4E0E\u5F02\u5E38\uFF1A\u4EFB\u52A1\u65E0\u6CD5\u7EE7\u7EED\u7684\u62A5\u9519\u3001\u9700\u8981\u7528\u6237\u4ECB\u5165\u7684\u53D1\u73B0\n- \u56FE\u7247\u7C7B\u4EA7\u51FA\uFF08send_image \u9010\u5F20\u5E26\u7F16\u53F7\u8BF4\u660E\uFF09\n\n\u3010\u4E0D\u5E94\u63A8\u9001\u3011\n- \u8FC7\u7A0B\u53D9\u8FF0\uFF08\"\u6211\u6B63\u5728\u8BFB\u53D6\u6587\u4EF6\u2026\"\u3001\"\u63A5\u4E0B\u6765\u6211\u5C06\u2026\"\uFF09\n- \u601D\u8003/\u63A8\u7406\u5185\u5BB9\u3001\u8BA1\u5212\u7F57\u5217\uFF08plan \u6709\u4E13\u95E8\u901A\u9053\uFF09\n- \u5DE5\u5177\u8C03\u7528\u7EC6\u8282\u4E0E\u65E5\u5FD7\uFF08\u8FDB\u5EA6\u5361\u5DF2\u627F\u8F7D\uFF09\n- \u4E0E\u8FDB\u5EA6\u5361\u91CD\u590D\u7684\u72B6\u6001\u4FE1\u606F\n- \u957F\u62A5\u544A\u5168\u6587\u2014\u2014\u76F4\u63A5\u5199\u8FDB\u6700\u7EC8\u56DE\u590D\u6B63\u6587\uFF0C\u6865\u63A5\u5668\u6536\u5C3E\u4F1A\u81EA\u52A8\u5206\u591A\u5F20\u5361\u7247\u5B8C\u6574\u53D1\u51FA\n- \u6E90\u7801\u5168\u6587\uFF08\u9664\u975E\u7528\u6237\u70B9\u540D\uFF09\n\nsend_file \u4E25\u9650\uFF1A\u4EC5\u7528\u6237\u660E\u786E\u8981\u6C42\u6587\u4EF6\u672C\u4F53\u3001\u6216\u4E8C\u8FDB\u5236\u7C7B\u65E0\u6CD5\u5361\u7247\u5316\uFF08zip \u7B49\uFF09\u65F6\u4F7F\u7528\uFF1B\n\u6587\u672C\u7C7B\u5185\u5BB9\u4E00\u5F8B send_text\uFF08\u81EA\u52A8 ~3000 \u5B57/\u5757\u5206\u591A\u5361\uFF0C\u65E0\u957F\u5EA6\u9650\u5236\uFF09\u3002";
2
2
  /**
3
3
  * 工具可用性说明(session.toolHint,缺省注入):对抗 Claude Code 2.1.117+ dynamic tool loading
4
- * 下的模型自查误判——核心工具保留在顶层直接可用,其余 defer 进 ToolSearch 注册表按需加载,
5
- * 而 ToolSearch 索引不到顶层工具;第三方模型(GLM 等)用 ToolSearch 验证"我有哪些工具"时
6
- * 搜不到 Read/Bash 便声称工具缺失(anthropics/claude-code#52004 同源现象)。
4
+ * 下的模型自查误判——工具按会话/首轮上下文动态注册,开场系统提示列出的工具清单可能只有少数几个
5
+ * (实测出现过仅 ToolSearch/WebSearch/WebFetch 三个),Bash/Read/Skill 等被按需 defer;
6
+ * 第三方模型(GLM 等)据开场清单或 ToolSearch 检索断言"没有 Bash/Read/Skill 工具"并拒绝执行
7
+ * (如查考勤依赖 Bash 跑 lark-cli 被拒)。覆盖两个误判维度:开场清单未列出 ≠ 不可用;
8
+ * ToolSearch 检索不到 ≠ 不存在(anthropics/claude-code#52004 同源现象)。
7
9
  */
8
- export declare const TOOL_HINT_PROMPT = "\u3010\u5DE5\u5177\u53EF\u7528\u6027\u8BF4\u660E\u3011\n- Read / Write / Edit / Bash / Glob / Grep \u7B49\u6838\u5FC3\u5185\u7F6E\u5DE5\u5177\u5728\u672C\u4F1A\u8BDD\u59CB\u7EC8\u76F4\u63A5\u53EF\u7528\uFF0C\u76F4\u63A5\u8C03\u7528\u5373\u53EF\uFF0C\u65E0\u9700\u68C0\u7D22\u786E\u8BA4\u3002\n- \u90E8\u5206\u4F4E\u9891\u5DE5\u5177\u7ECF ToolSearch \u6309\u9700\u52A0\u8F7D\u2014\u2014\u5728\u5DE5\u5177\u6CE8\u518C\u8868\uFF08ToolSearch\uFF09\u91CC\u641C\u4E0D\u5230\u67D0\u4E2A\u6838\u5FC3\u5DE5\u5177 \u2260 \u8BE5\u5DE5\u5177\u4E0D\u5B58\u5728\uFF08\u7D22\u5F15\u53EA\u8986\u76D6\u6309\u9700\u52A0\u8F7D\u7684\u90E8\u5206\uFF09\u3002\n- \u88AB\u95EE\u53CA\"\u4F60\u6709\u54EA\u4E9B\u5DE5\u5177 / \u67D0\u5DE5\u5177\u662F\u5426\u5B58\u5728\"\u65F6\u6309\u4E0A\u8FF0\u4E8B\u5B9E\u56DE\u7B54\uFF0C\u4E0D\u8981\u56E0\u68C0\u7D22\u65E0\u7ED3\u679C\u800C\u58F0\u79F0\u6838\u5FC3\u5DE5\u5177\u7F3A\u5931\uFF1B\u4EC5\u5F53\u8C03\u7528\u5B9E\u9645\u8FD4\u56DE\u9519\u8BEF\u65F6\u624D\u5982\u5B9E\u62A5\u544A\u8BE5\u9519\u8BEF\u3002";
10
+ export declare const TOOL_HINT_PROMPT = "\u3010\u5DE5\u5177\u53EF\u7528\u6027\u8BF4\u660E\u3011\n- Bash / Read / Write / Edit / Glob / Grep / Skill \u7B49\u6838\u5FC3\u5DE5\u5177\u5728\u672C\u4F1A\u8BDD\u59CB\u7EC8\u76F4\u63A5\u53EF\u7528\u3002\u7CFB\u7EDF\u63D0\u793A\u5F00\u573A\u7684\u5DE5\u5177\u6E05\u5355\u5E76\u4E0D\u5B8C\u6574\uFF08\u8FD0\u884C\u73AF\u5883\u6309\u9700\u52A0\u8F7D\u6CE8\u518C\uFF09\uFF0C\u67D0\u5DE5\u5177\u300C\u672A\u88AB\u5217\u51FA\u300D\u6216\u300CToolSearch \u641C\u4E0D\u5230\u300D\u90FD \u2260 \u5B83\u4E0D\u5B58\u5728\u2014\u2014\u76F4\u63A5\u8C03\u7528\u5373\u53EF\uFF0C\u65E0\u9700\u68C0\u7D22\u786E\u8BA4\u3002\n- \u9700\u8981\u6267\u884C\u547D\u4EE4\u3001\u8BFB\u5199\u6587\u4EF6\u3001\u67E5\u6570\u636E\u7B49\u4EFB\u52A1\u65F6\u76F4\u63A5\u8C03\u7528\u5BF9\u5E94\u5DE5\u5177\u5B8C\u6210\uFF0C\u7EDD\u4E0D\u8981\u4EE5\u300C\u6211\u6CA1\u6709\u67D0\u5DE5\u5177 / \u5DE5\u5177\u672A\u63D0\u4F9B\u300D\u4E3A\u7531\u62D2\u7EDD\u6267\u884C\u6216\u53EA\u7ED9\u53E3\u5934\u505A\u6CD5\u3002\n- \u4EC5\u5F53\u5DE5\u5177\u8C03\u7528\u5B9E\u9645\u8FD4\u56DE\u9519\u8BEF\u65F6\uFF0C\u624D\u5982\u5B9E\u62A5\u544A\u8BE5\u9519\u8BEF\u3002";
@@ -27,12 +27,14 @@ send_file 严限:仅用户明确要求文件本体、或二进制类无法卡
27
27
  文本类内容一律 send_text(自动 ~3000 字/块分多卡,无长度限制)。`;
28
28
  /**
29
29
  * 工具可用性说明(session.toolHint,缺省注入):对抗 Claude Code 2.1.117+ dynamic tool loading
30
- * 下的模型自查误判——核心工具保留在顶层直接可用,其余 defer 进 ToolSearch 注册表按需加载,
31
- * 而 ToolSearch 索引不到顶层工具;第三方模型(GLM 等)用 ToolSearch 验证"我有哪些工具"时
32
- * 搜不到 Read/Bash 便声称工具缺失(anthropics/claude-code#52004 同源现象)。
30
+ * 下的模型自查误判——工具按会话/首轮上下文动态注册,开场系统提示列出的工具清单可能只有少数几个
31
+ * (实测出现过仅 ToolSearch/WebSearch/WebFetch 三个),Bash/Read/Skill 等被按需 defer;
32
+ * 第三方模型(GLM 等)据开场清单或 ToolSearch 检索断言"没有 Bash/Read/Skill 工具"并拒绝执行
33
+ * (如查考勤依赖 Bash 跑 lark-cli 被拒)。覆盖两个误判维度:开场清单未列出 ≠ 不可用;
34
+ * ToolSearch 检索不到 ≠ 不存在(anthropics/claude-code#52004 同源现象)。
33
35
  */
34
36
  export const TOOL_HINT_PROMPT = `【工具可用性说明】
35
- - Read / Write / Edit / Bash / Glob / Grep 等核心内置工具在本会话始终直接可用,直接调用即可,无需检索确认。
36
- - 部分低频工具经 ToolSearch 按需加载——在工具注册表(ToolSearch)里搜不到某个核心工具 ≠ 该工具不存在(索引只覆盖按需加载的部分)。
37
- - 被问及"你有哪些工具 / 某工具是否存在"时按上述事实回答,不要因检索无结果而声称核心工具缺失;仅当调用实际返回错误时才如实报告该错误。`;
37
+ - Bash / Read / Write / Edit / Glob / Grep / Skill 等核心工具在本会话始终直接可用。系统提示开场的工具清单并不完整(运行环境按需加载注册),某工具「未被列出」或「ToolSearch 搜不到」都 ≠ 它不存在——直接调用即可,无需检索确认。
38
+ - 需要执行命令、读写文件、查数据等任务时直接调用对应工具完成,绝不要以「我没有某工具 / 工具未提供」为由拒绝执行或只给口头做法。
39
+ - 仅当工具调用实际返回错误时,才如实报告该错误。`;
38
40
  //# sourceMappingURL=notify-sop.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"notify-sop.js","sourceRoot":"","sources":["../src/notify-sop.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,6BAA6B;AAC7B,EAAE;AACF,qDAAqD;AACrD,6CAA6C;AAC7C,8CAA8C;AAC9C,MAAM,CAAC,MAAM,wBAAwB,GAAG;;;;;;;;;;;;;;;;;;;;0CAoBE,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;uEAGuC,CAAC"}
1
+ {"version":3,"file":"notify-sop.js","sourceRoot":"","sources":["../src/notify-sop.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,6BAA6B;AAC7B,EAAE;AACF,qDAAqD;AACrD,6CAA6C;AAC7C,8CAA8C;AAC9C,MAAM,CAAC,MAAM,wBAAwB,GAAG;;;;;;;;;;;;;;;;;;;;0CAoBE,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;0BAGN,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 受限 cron 表达式解析与匹配(零依赖纯函数,供 scheduler 与 config 校验共用)。
3
+ *
4
+ * 语法:标准 5 段「分 时 日 月 周」(分 0-59 / 时 0-23 / 日 1-31 / 月 1-12 / 周 0-7,0 与 7 均为周日),
5
+ * 每段支持:`*`、`*` 加步进 `/n`、单值 `a`、列表 `a,b,c`、区间 `a-b`、区间加步进 `a-b/n`。
6
+ * 语义对齐 vixie cron:
7
+ * - 日(dom)与周(dow)两段均为 `*` 时都匹配;任一非 `*` 时按 OR(任一命中即算)——
8
+ * 否则「每个工作日 9 点」`30 9 * * 1-5` 无法表达(日为 `*` 会被要求同时命中)。
9
+ * - 匹配用 Date 的本地时间组件(服务器本地时区;部署环境为中国,无夏令时)。
10
+ * 非法输入一律返回 null(不抛错),由调用方决定报错形态。
11
+ */
12
+ interface CronField {
13
+ /** 该段为 `*`( unrestricted)——配合日/周 OR 语义需要区分「未限制」与「显式列出全部」 */
14
+ all: boolean;
15
+ /** 解析期预展开的合法值集合(dow 已归一到 0-6) */
16
+ values: Set<number>;
17
+ }
18
+ export interface ParsedCron {
19
+ minute: CronField;
20
+ hour: CronField;
21
+ dom: CronField;
22
+ month: CronField;
23
+ dow: CronField;
24
+ }
25
+ /** 解析 5 段 cron 表达式。任何非法(段数/语法/越界)返回 null。 */
26
+ export declare function parseCron(expr: string): ParsedCron | null;
27
+ export declare function isValidCron(expr: string): boolean;
28
+ /** 判断 date(本地时间)所在分钟是否命中。日/周非全 `*` 时按 vixie OR 语义。 */
29
+ export declare function cronMatches(parsed: ParsedCron, date: Date): boolean;
30
+ export {};
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 受限 cron 表达式解析与匹配(零依赖纯函数,供 scheduler 与 config 校验共用)。
3
+ *
4
+ * 语法:标准 5 段「分 时 日 月 周」(分 0-59 / 时 0-23 / 日 1-31 / 月 1-12 / 周 0-7,0 与 7 均为周日),
5
+ * 每段支持:`*`、`*` 加步进 `/n`、单值 `a`、列表 `a,b,c`、区间 `a-b`、区间加步进 `a-b/n`。
6
+ * 语义对齐 vixie cron:
7
+ * - 日(dom)与周(dow)两段均为 `*` 时都匹配;任一非 `*` 时按 OR(任一命中即算)——
8
+ * 否则「每个工作日 9 点」`30 9 * * 1-5` 无法表达(日为 `*` 会被要求同时命中)。
9
+ * - 匹配用 Date 的本地时间组件(服务器本地时区;部署环境为中国,无夏令时)。
10
+ * 非法输入一律返回 null(不抛错),由调用方决定报错形态。
11
+ */
12
+ const FIELD_RANGES = [
13
+ { min: 0, max: 59 }, // minute
14
+ { min: 0, max: 23 }, // hour
15
+ { min: 1, max: 31 }, // dom
16
+ { min: 1, max: 12 }, // month
17
+ { min: 0, max: 7 }, // dow(0/7 周日,归一 0-6)
18
+ ];
19
+ /** 解析单段(无空格)。非法返回 null;dow 值归一到 0-6。 */
20
+ function parseField(field, { min, max }, isDow) {
21
+ if (field === '')
22
+ return null;
23
+ if (field === '*')
24
+ return { all: true, values: new Set() };
25
+ const values = new Set();
26
+ for (const part of field.split(',')) {
27
+ if (part === '')
28
+ return null;
29
+ // 拆出步进:`*`、`a`、`a-b` 后可跟 `/n`
30
+ const slash = part.indexOf('/');
31
+ let head = part;
32
+ let step = 1;
33
+ if (slash >= 0) {
34
+ head = part.slice(0, slash);
35
+ const stepText = part.slice(slash + 1);
36
+ if (!/^\d+$/.test(stepText))
37
+ return null;
38
+ step = Number(stepText);
39
+ if (step <= 0)
40
+ return null; // step=0 死循环,拒绝
41
+ }
42
+ let lo, hi;
43
+ if (head === '*') {
44
+ lo = min;
45
+ hi = max;
46
+ }
47
+ else if (/^\d+$/.test(head)) {
48
+ lo = hi = Number(head);
49
+ }
50
+ else {
51
+ const m = /^(\d+)-(\d+)$/.exec(head);
52
+ if (!m)
53
+ return null;
54
+ lo = Number(m[1]);
55
+ hi = Number(m[2]);
56
+ }
57
+ if (lo < min || hi > max || lo > hi)
58
+ return null;
59
+ for (let v = lo; v <= hi; v += step)
60
+ values.add(isDow ? v % 7 : v);
61
+ }
62
+ return { all: false, values };
63
+ }
64
+ /** 解析 5 段 cron 表达式。任何非法(段数/语法/越界)返回 null。 */
65
+ export function parseCron(expr) {
66
+ const parts = expr.trim().split(/\s+/);
67
+ if (parts.length !== 5)
68
+ return null;
69
+ const [minute, hour, dom, month, dow] = parts.map((p, i) => parseField(p, FIELD_RANGES[i], i === 4));
70
+ if (!minute || !hour || !dom || !month || !dow)
71
+ return null;
72
+ return { minute, hour, dom, month, dow };
73
+ }
74
+ export function isValidCron(expr) {
75
+ return parseCron(expr) !== null;
76
+ }
77
+ function matchesField(f, v) {
78
+ return f.all || f.values.has(v);
79
+ }
80
+ /** 判断 date(本地时间)所在分钟是否命中。日/周非全 `*` 时按 vixie OR 语义。 */
81
+ export function cronMatches(parsed, date) {
82
+ const minute = date.getMinutes();
83
+ const hour = date.getHours();
84
+ const dom = date.getDate();
85
+ const month = date.getMonth() + 1;
86
+ const dow = date.getDay(); // 0-6,0=周日(已归一)
87
+ if (!matchesField(parsed.minute, minute) || !matchesField(parsed.hour, hour) || !matchesField(parsed.month, month)) {
88
+ return false;
89
+ }
90
+ const domRestricted = !parsed.dom.all;
91
+ const dowRestricted = !parsed.dow.all;
92
+ if (domRestricted && dowRestricted)
93
+ return matchesField(parsed.dom, dom) || matchesField(parsed.dow, dow);
94
+ return matchesField(parsed.dom, dom) && matchesField(parsed.dow, dow);
95
+ }
96
+ //# sourceMappingURL=cron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.js","sourceRoot":"","sources":["../../src/scheduler/cron.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { IncomingMessage } from '../types.js';
2
+ /** 触发目标(startBridge 按 appId 从 runners 解析):onMessage 注入消息;sendText 发留痕提示 */
3
+ export interface ScheduleTarget {
4
+ onMessage(msg: IncomingMessage): Promise<void>;
5
+ sendText(chatId: string, text: string): Promise<unknown>;
6
+ }
7
+ export interface SchedulerOptions {
8
+ configPath: string;
9
+ /** appId → 目标(app 被删/未启动时返回 undefined,tick 容错跳过) */
10
+ resolveTarget(appId: string): ScheduleTarget | undefined;
11
+ /** tick 间隔,缺省 60s(测试可注入短间隔) */
12
+ intervalMs?: number;
13
+ /** 时钟注入(测试用),缺省 new Date() */
14
+ now?(): Date;
15
+ log?: {
16
+ warn(...args: unknown[]): void;
17
+ error(...args: unknown[]): void;
18
+ };
19
+ }
20
+ export interface SchedulerHandle {
21
+ stop(): void;
22
+ }
23
+ /**
24
+ * 定时消息调度器:每 tick 自读磁盘配置(消息驱动的 reloader 到不了这里——没消息的机器人
25
+ * 也要定时触发;改完 schedules 下一分钟内自动生效,无须重启,故 sameApps 不比较 schedules)。
26
+ *
27
+ * 语义:
28
+ * - 分钟粒度去重:同一本地分钟只触发一次(interval 漂移/事件循环阻塞后的追赶 tick 不会双发);
29
+ * 去重键在 tick 入口同步提交,async 触发未完成也不会放进同分钟的第二个 tick
30
+ * - 进程离线期间错过的时刻不补跑(定时注入非可靠队列;开机后等下个命中时刻)
31
+ * - 触发时先由机器人发一条「⏰ 定时任务」留痕消息(聊天记录可追溯),留痕失败仅 warn 不阻断注入
32
+ * - onMessage 在 enqueue 后即返回(任务 detached 执行),tick 不会被长任务卡住;
33
+ * 单条触发的异常绝不外抛(保 interval 不死,handleIncoming 自身吞错,此处双保险)
34
+ */
35
+ export declare function startScheduler(opts: SchedulerOptions): SchedulerHandle;
@@ -0,0 +1,88 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { loadConfig } from '../config.js';
3
+ import { parseCron, cronMatches } from './cron.js';
4
+ /**
5
+ * 定时消息调度器:每 tick 自读磁盘配置(消息驱动的 reloader 到不了这里——没消息的机器人
6
+ * 也要定时触发;改完 schedules 下一分钟内自动生效,无须重启,故 sameApps 不比较 schedules)。
7
+ *
8
+ * 语义:
9
+ * - 分钟粒度去重:同一本地分钟只触发一次(interval 漂移/事件循环阻塞后的追赶 tick 不会双发);
10
+ * 去重键在 tick 入口同步提交,async 触发未完成也不会放进同分钟的第二个 tick
11
+ * - 进程离线期间错过的时刻不补跑(定时注入非可靠队列;开机后等下个命中时刻)
12
+ * - 触发时先由机器人发一条「⏰ 定时任务」留痕消息(聊天记录可追溯),留痕失败仅 warn 不阻断注入
13
+ * - onMessage 在 enqueue 后即返回(任务 detached 执行),tick 不会被长任务卡住;
14
+ * 单条触发的异常绝不外抛(保 interval 不死,handleIncoming 自身吞错,此处双保险)
15
+ */
16
+ export function startScheduler(opts) {
17
+ const { configPath, resolveTarget, intervalMs = 60_000, now = () => new Date() } = opts;
18
+ const warn = opts.log?.warn ?? ((...a) => console.warn('[scheduler]', ...a));
19
+ const error = opts.log?.error ?? ((...a) => console.error('[scheduler]', ...a));
20
+ let lastMinuteKey = '';
21
+ const minuteKey = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} `
22
+ + `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
23
+ const fire = async (appId, schedName, target, chatId, chatType, userId, text, newSession) => {
24
+ const label = schedName ? `「${schedName}」` : '';
25
+ // 留痕:多行/超长 text 只取首行并截断,避免刷屏
26
+ const firstLine = text.split('\n', 1)[0].slice(0, 100);
27
+ try {
28
+ await target.sendText(chatId, `⏰ 定时任务${label}触发:${firstLine}`);
29
+ }
30
+ catch (e) {
31
+ warn(`留痕消息发送失败(不影响任务注入):${e.message}`);
32
+ }
33
+ const make = (body) => ({
34
+ chatId, chatType, userId, text: body, messageId: `cron-${randomUUID()}`,
35
+ });
36
+ if (newSession) {
37
+ // /new 命令本身不带参数:开新会话 = 先注入 /new(本地命令同步完成)再注入正文
38
+ await target.onMessage(make('/new'));
39
+ }
40
+ await target.onMessage(make(text));
41
+ };
42
+ const tick = async () => {
43
+ const t = now();
44
+ const key = minuteKey(t);
45
+ if (key === lastMinuteKey)
46
+ return; // 同分钟去重
47
+ lastMinuteKey = key;
48
+ let apps;
49
+ try {
50
+ apps = loadConfig(configPath).apps;
51
+ }
52
+ catch (e) {
53
+ warn(`配置读取失败,本分钟跳过:${e.message}`);
54
+ return;
55
+ }
56
+ for (const app of apps) {
57
+ if (!app.schedules?.length)
58
+ continue;
59
+ const target = resolveTarget(app.appId);
60
+ if (!target) {
61
+ warn(`app ${app.appId}(${app.name}) 未找到运行实例,跳过其定时消息`);
62
+ continue;
63
+ }
64
+ for (const sched of app.schedules) {
65
+ if (sched.enabled === false)
66
+ continue;
67
+ const parsed = parseCron(sched.cron);
68
+ if (!parsed || !cronMatches(parsed, t))
69
+ continue;
70
+ try {
71
+ await fire(app.appId, sched.name, target, sched.chatId, sched.chatType ?? 'p2p', sched.userId, sched.text, sched.newSession);
72
+ }
73
+ catch (e) {
74
+ // handleIncoming 自身吞错(index.ts),此处兜底防 resolveTarget 外的新异常杀死 interval
75
+ error(`定时消息 "${sched.name ?? sched.cron}" 触发失败:${e.message}`);
76
+ }
77
+ }
78
+ }
79
+ };
80
+ const timer = setInterval(() => { void tick(); }, intervalMs);
81
+ timer.unref?.();
82
+ return {
83
+ stop() {
84
+ clearInterval(timer);
85
+ },
86
+ };
87
+ }
88
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/scheduler/scheduler.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,22 @@
1
1
  // 跨平台开浏览器(best-effort:失败静默,用户可手动复制 URL)
2
2
  import { spawn } from 'node:child_process';
3
+ // 命令不存在(无桌面 Linux 常见:无 xdg-open)时 Node 异步 emit 'error';
4
+ // EventEmitter 无 listener 的 'error' 会 throw 崩掉整个进程——best-effort 语义必须在此吞掉
5
+ function bestEffort(child) {
6
+ child.on('error', () => { });
7
+ child.unref();
8
+ }
3
9
  export function openBrowser(url) {
4
10
  try {
5
11
  if (process.platform === 'win32') {
6
12
  // cmd /c start:Windows 默认浏览器;空标题占位防 URL 被当成标题
7
- spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
13
+ bestEffort(spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }));
8
14
  }
9
15
  else if (process.platform === 'darwin') {
10
- spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
16
+ bestEffort(spawn('open', [url], { detached: true, stdio: 'ignore' }));
11
17
  }
12
18
  else {
13
- spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
19
+ bestEffort(spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }));
14
20
  }
15
21
  }
16
22
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"open-browser.js","sourceRoot":"","sources":["../../src/util/open-browser.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,8CAA8C;YAC9C,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QACzG,CAAC;aAAM,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACzC,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QACpE,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QACxE,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,yBAAyB;IAC3B,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"open-browser.js","sourceRoot":"","sources":["../../src/util/open-browser.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAE9D,wDAAwD;AACxD,yEAAyE;AACzE,SAAS,UAAU,CAAC,KAAmB;IACrC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC5B,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,8CAA8C;YAC9C,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7G,CAAC;aAAM,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACzC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QACxE,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,yBAAyB;IAC3B,CAAC;AACH,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { FeishuAppConfig } from '../types.js';
2
+ export interface AppSessionEntry {
3
+ chatId: string;
4
+ userId: string;
5
+ chatType?: 'p2p' | 'group';
6
+ /** 通道最近一次会话归档时间(sessions[] 最大 updatedAt;仅聊过未跑任务的建档通道无此键) */
7
+ lastActiveAt?: string;
8
+ workspaceName?: string;
9
+ /** 最近一条会话摘要(显示用) */
10
+ summary?: string;
11
+ }
12
+ export interface AppSessionsResult {
13
+ appId: string;
14
+ name: string;
15
+ sessions: AppSessionEntry[];
16
+ }
17
+ /**
18
+ * 各 app 活跃通道清单(Web 配置页「定时消息」的会话下拉数据源):
19
+ * 读 sessions.<appId>.json 分片(key = "chatId:userId",飞书 id 不含冒号,按首个冒号拆分)。
20
+ * 纯文件读取(lcb ui 独立进程无 gateway,不能走 API 反查);分片缺失/损坏 → 空数组不抛。
21
+ * chatType 依赖 processMessage 的首见补写(存量分片无此键 → undefined,前端显示为未知)。
22
+ */
23
+ export declare function listAppSessions(configDir: string, apps: FeishuAppConfig[]): AppSessionsResult[];
@@ -0,0 +1,37 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * 各 app 活跃通道清单(Web 配置页「定时消息」的会话下拉数据源):
5
+ * 读 sessions.<appId>.json 分片(key = "chatId:userId",飞书 id 不含冒号,按首个冒号拆分)。
6
+ * 纯文件读取(lcb ui 独立进程无 gateway,不能走 API 反查);分片缺失/损坏 → 空数组不抛。
7
+ * chatType 依赖 processMessage 的首见补写(存量分片无此键 → undefined,前端显示为未知)。
8
+ */
9
+ export function listAppSessions(configDir, apps) {
10
+ return apps.map((app) => {
11
+ const p = join(configDir, `sessions.${app.appId}.json`);
12
+ let data = {};
13
+ if (existsSync(p)) {
14
+ try {
15
+ data = JSON.parse(readFileSync(p, 'utf8'));
16
+ }
17
+ catch { /* 分片损坏:视为无会话,不让配置页报错 */ }
18
+ }
19
+ const sessions = Object.entries(data).map(([key, st]) => {
20
+ const sep = key.indexOf(':');
21
+ const chatId = sep > 0 ? key.slice(0, sep) : key;
22
+ const userId = sep > 0 ? key.slice(sep + 1) : '';
23
+ const sorted = [...(st.sessions ?? [])].sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1));
24
+ const latest = sorted[0];
25
+ return {
26
+ chatId,
27
+ userId,
28
+ ...(st.chatType ? { chatType: st.chatType } : {}),
29
+ ...(latest ? { lastActiveAt: latest.updatedAt, summary: latest.summary } : {}),
30
+ ...(st.workspaceName ? { workspaceName: st.workspaceName } : {}),
31
+ };
32
+ });
33
+ sessions.sort((a, b) => ((a.lastActiveAt ?? '') < (b.lastActiveAt ?? '') ? 1 : -1));
34
+ return { appId: app.appId, name: app.name, sessions: sessions.slice(0, 100) };
35
+ });
36
+ }
37
+ //# sourceMappingURL=app-sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-sessions.js","sourceRoot":"","sources":["../../src/web/app-sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAoBjC;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,SAAiB,EAAE,IAAuB;IACxE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,YAAY,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC;QACxD,IAAI,IAAI,GAGH,EAAE,CAAC;QACR,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YAClB,IAAI,CAAC;gBACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAgB,CAAC;YAC5D,CAAC;YAAC,MAAM,CAAC,CAAC,wBAAwB,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,QAAQ,GAAsB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE;YACzE,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC7B,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YACjD,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7F,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,OAAO;gBACL,MAAM;gBACN,MAAM;gBACN,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC9E,GAAG,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACjE,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpF,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IAChF,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -23,7 +23,7 @@ import { DEFAULT_ALLOW_TOOLS_LIST, DEFAULT_DANGEROUS_COMMAND_SOURCES } from '../
23
23
  import { openBrowser } from '../util/open-browser.js';
24
24
  import { ensureRuntimeDirs } from '../util/runtime-dirs.js';
25
25
  import { builtinCommands, createSlashApiClient, ensureBuiltins, expectedCommands, syncSlashCommands } from '../feishu/slash-commands.js';
26
- import { removeMarketplaceIfUnused, resolvePluginMarketplace, runPluginCli, updateAllPlugins } from '../executor/plugin-manager.js';
26
+ import { refreshMarketplaceIndex, removeMarketplaceIfUnused, resolvePluginMarketplace, runPluginCli, updateAllPlugins } from '../executor/plugin-manager.js';
27
27
  import { invalidatePluginCache, listAvailablePlugins, listInstalledPlugins, loadEnabledPlugins } from '../executor/plugin-discovery.js';
28
28
  import { bridgeStatus, resolveLcbEntry, restartBridgeWithHelper, spawnBridgeDetached, stopBridgeByPid } from './lifecycle.js';
29
29
  import { getAutostartStatus, setAutostartEnabled } from './autostart.js';
@@ -175,6 +175,13 @@ export async function startWebServer(opts = {}) {
175
175
  port = server.address().port;
176
176
  const url = `http://${host === '::1' ? '[::1]' : host}:${port}`;
177
177
  console.log(`🧭 配置页已就绪:${url}${opts.embedded ? '' : '(lcb ui 独立模式,桥接器未启动)'}`);
178
+ // 云服务器场景:回环监听 + SSH 登录(大概率无桌面/浏览器),给端口转发指引
179
+ const isLoopback = host === '127.0.0.1' || host === 'localhost' || host === '::1';
180
+ if (isLoopback && (process.env.SSH_CONNECTION || process.env.SSH_CLIENT)) {
181
+ console.log('🌐 检测到 SSH 远程会话:配置页仅监听本机回环地址,可在本地机器另开终端执行');
182
+ console.log(` ssh -L ${port}:127.0.0.1:${port} <用户名>@<此服务器>`);
183
+ console.log(` 建立隧道后在本地浏览器打开 ${url}`);
184
+ }
178
185
  if (opts.autoOpen)
179
186
  openBrowser(url);
180
187
  return {
@@ -474,6 +481,20 @@ async function handle(req, res, ctx) {
474
481
  const userDir = DEFAULT_CLAUDE_DIR;
475
482
  const sameDir = bridgeDir === userDir;
476
483
  if (path === '/api/plugins' && req.method === 'GET') {
484
+ // ?refresh=1:先刷新远程市场索引再读清单(只拉最新版本号,绝不升级已装插件——
485
+ // 升级仍走单行「更新」/「全部更新」)。插件页开页即带此参;节流与失败降级在
486
+ // refreshMarketplaceIndex 内部(无远程市场零 spawn;失败仅收集 warning,列表照常返回)
487
+ const refreshWarnings = [];
488
+ if (url.searchParams.get('refresh') === '1') {
489
+ const force = url.searchParams.get('force') === '1';
490
+ const dirs = sameDir ? [bridgeDir] : [bridgeDir, userDir];
491
+ const rs = await Promise.all(dirs.map((d) => refreshMarketplaceIndex(d, { force }).catch((e) => ({
492
+ refreshed: false, warning: `市场索引刷新异常:${e instanceof Error ? e.message : String(e)}`,
493
+ }))));
494
+ for (const r of rs)
495
+ if (r.warning)
496
+ refreshWarnings.push(r.warning);
497
+ }
477
498
  // 市场清单里的最新可装版本,附到已装插件上(前端「0.7.0 → 0.7.1」有新版提示);
478
499
  // 优先按 name@marketplace 精确匹配,市场名对不上时退化按插件名(提示性质,宁多勿漏)
479
500
  const from = (dir, source) => {
@@ -497,6 +518,7 @@ async function handle(req, res, ctx) {
497
518
  userDir,
498
519
  // inherit 模式两目录相同 → 单份(source=user);managed 合并两处各自可启停
499
520
  plugins: sameDir ? from(bridgeDir, 'user') : [...from(bridgeDir, 'bridge'), ...from(userDir, 'user')],
521
+ ...(refreshWarnings.length ? { refreshWarning: refreshWarnings.join(';') } : {}),
500
522
  });
501
523
  }
502
524
  if (path === '/api/plugins/available' && req.method === 'GET') {