opencode-feishu-plugin 0.1.0 → 0.1.1

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/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { EventEmitter } from 'events';
11
11
  import fs, { mkdirSync, createWriteStream, readFileSync, statSync, realpathSync } from 'fs';
12
12
  import qs from 'querystring';
13
13
  import { homedir } from 'os';
14
- import { readdir, readFile } from 'fs/promises';
14
+ import { readFile, readdir } from 'fs/promises';
15
15
 
16
16
  var __create = Object.create;
17
17
  var __defProp = Object.defineProperty;
@@ -146690,6 +146690,410 @@ function decideRoute(facts) {
146690
146690
  if (facts.rootKnown) return { kind: "use-session", source: "root" };
146691
146691
  return { kind: "create-in-thread" };
146692
146692
  }
146693
+ var SYSTEM_DIRS = [
146694
+ "/etc",
146695
+ "/usr",
146696
+ "/bin",
146697
+ "/sbin",
146698
+ "/lib",
146699
+ "/lib64",
146700
+ "/boot",
146701
+ "/dev",
146702
+ "/proc",
146703
+ "/sys",
146704
+ "/run",
146705
+ "/root",
146706
+ "/var"
146707
+ ];
146708
+ function isUnder(child, root) {
146709
+ if (!root || root === "/") return true;
146710
+ const normalizedRoot = root.endsWith(sep) ? root.slice(0, -1) : root;
146711
+ return child === normalizedRoot || child.startsWith(`${normalizedRoot}${sep}`);
146712
+ }
146713
+ function isSystemPath(path2) {
146714
+ return SYSTEM_DIRS.some((dir) => isUnder(path2, dir));
146715
+ }
146716
+ function validateDirectory(rawPath, allowedRoots, deps = {}) {
146717
+ const raw = rawPath.trim();
146718
+ const roots = allowedRoots.map((r) => resolve$1(r)).filter((r) => r && r !== "");
146719
+ if (roots.length === 0) {
146720
+ return err("empty", "\u672A\u914D\u7F6E\u5141\u8BB8\u7684\u5DE5\u4F5C\u76EE\u5F55\u6839\uFF08`allowedRoots`\uFF09\uFF0C\u65E0\u6CD5\u786E\u5B9A\u9ED8\u8BA4\u76EE\u5F55\u3002");
146721
+ }
146722
+ let target;
146723
+ if (!raw) {
146724
+ target = roots[0];
146725
+ } else {
146726
+ if (!isAbsolute(raw)) return err("not_absolute", "\u76EE\u5F55\u5FC5\u987B\u662F**\u7EDD\u5BF9\u8DEF\u5F84**\uFF08\u4EE5 `/` \u5F00\u5934\uFF09\u3002");
146727
+ target = resolve$1(raw);
146728
+ }
146729
+ const stat = deps.statSync ?? statSync;
146730
+ const realpath = deps.realpathSync ?? realpathSync;
146731
+ const mkdir = deps.mkdirSync ?? mkdirSync;
146732
+ const blocked = forbiddenReason(target);
146733
+ if (blocked) return blocked;
146734
+ if (!roots.some((root) => isUnder(target, root))) {
146735
+ return err("outside_allowed", outsideMessage(roots));
146736
+ }
146737
+ let exists2 = true;
146738
+ try {
146739
+ if (!stat(target).isDirectory()) return err("not_dir", `\u4E0D\u662F\u76EE\u5F55\uFF1A\`${target}\``);
146740
+ } catch {
146741
+ exists2 = false;
146742
+ }
146743
+ if (!exists2) {
146744
+ try {
146745
+ mkdir(target, { recursive: true });
146746
+ } catch {
146747
+ return err("create_failed", `\u76EE\u5F55\u4E0D\u5B58\u5728\u4E14\u65E0\u6CD5\u521B\u5EFA\uFF1A\`${target}\`\u3002\u8BF7\u68C0\u67E5\u8DEF\u5F84\u4E0E\u6743\u9650\u3002`);
146748
+ }
146749
+ }
146750
+ let real = target;
146751
+ try {
146752
+ real = realpath(target);
146753
+ } catch {
146754
+ real = target;
146755
+ }
146756
+ const blockedReal = forbiddenReason(real);
146757
+ if (blockedReal) return blockedReal;
146758
+ if (!roots.some((root) => isUnder(real, root))) {
146759
+ return err("outside_allowed", outsideMessage(roots));
146760
+ }
146761
+ return { ok: true, path: real };
146762
+ }
146763
+ function outsideMessage(roots) {
146764
+ return `\u76EE\u5F55\u4E0D\u5728\u5141\u8BB8\u8303\u56F4\u5185\u3002\u5141\u8BB8\u7684\u6839\u76EE\u5F55\uFF1A${roots.map((r) => `\`${r}\``).join("\u3001")}\u3002`;
146765
+ }
146766
+ function forbiddenReason(path2) {
146767
+ if (path2 === sep) return err("forbidden", "\u4E0D\u80FD\u4F7F\u7528\u6839\u76EE\u5F55 `/`\uFF0C\u8BF7\u9009\u62E9\u5177\u4F53\u7684\u9879\u76EE\u5B50\u76EE\u5F55\u3002");
146768
+ if (isSystemPath(path2)) return err("system", `\u7CFB\u7EDF\u76EE\u5F55\u4E0D\u53EF\u4F5C\u4E3A\u5DE5\u4F5C\u76EE\u5F55\uFF1A\`${path2}\`\u3002`);
146769
+ return void 0;
146770
+ }
146771
+ function err(reason, message) {
146772
+ return { ok: false, reason, message };
146773
+ }
146774
+
146775
+ // src/feishu/perm-presets.ts
146776
+ var SHELL_ACTIONS = ["shell", "bash"];
146777
+ var PERMISSION_PRESETS = [
146778
+ { id: "readonly", icon: "\u{1F512}", label: "\u53EA\u8BFB", description: "\u7981\u6B62\u7F16\u8F91 / \u6267\u884C / \u5199\u5165\uFF0C\u6700\u5B89\u5168\u3002" },
146779
+ { id: "edit", icon: "\u270F\uFE0F", label: "\u53EF\u7F16\u8F91", description: "\u5141\u8BB8\u6539\u6587\u4EF6\uFF1B\u6267\u884C\u547D\u4EE4\u9700\u4F60\u5BA1\u6279\u3002" },
146780
+ {
146781
+ id: "askHigh",
146782
+ icon: "\u26A0\uFE0F",
146783
+ label: "\u9AD8\u98CE\u9669\u5BA1\u6279",
146784
+ description: "\u7EE7\u627F\u9ED8\u8BA4\u89C4\u5219\uFF0C\u5BF9 shell / \u7F16\u8F91 / \u5916\u90E8\u76EE\u5F55\u9010\u6B21\u5BA1\u6279\u3002"
146785
+ },
146786
+ { id: "trust", icon: "\u{1F513}", label: "\u5B8C\u5168\u4FE1\u4EFB", description: "\u653E\u884C\u5168\u90E8\u64CD\u4F5C\uFF0C\u8BF7\u8C28\u614E\u4F7F\u7528\u3002" }
146787
+ ];
146788
+ var ASK_ACTIONS = {
146789
+ readonly: [],
146790
+ edit: ["shell", "bash"],
146791
+ askHigh: ["shell", "bash", "edit", "external_directory"],
146792
+ trust: []
146793
+ };
146794
+ var PRESET_IDS = PERMISSION_PRESETS.map((p) => p.id);
146795
+ function isPermissionPreset(value3) {
146796
+ return typeof value3 === "string" && PRESET_IDS.includes(value3);
146797
+ }
146798
+ function presetInfo(preset) {
146799
+ return PERMISSION_PRESETS.find((p) => p.id === preset) ?? PERMISSION_PRESETS[1];
146800
+ }
146801
+ function presetLabel(preset) {
146802
+ const info = presetInfo(preset);
146803
+ return `${info.icon} ${info.label}`;
146804
+ }
146805
+ function presetToRuleset(preset) {
146806
+ switch (preset) {
146807
+ case "readonly": {
146808
+ const actions = ["edit", "write", ...SHELL_ACTIONS];
146809
+ return actions.map((action) => ({ action, resource: "*", effect: "deny" }));
146810
+ }
146811
+ case "edit": {
146812
+ return [
146813
+ { action: "edit", resource: "*", effect: "allow" },
146814
+ ...SHELL_ACTIONS.map((action) => ({ action, resource: "*", effect: "ask" }))
146815
+ ];
146816
+ }
146817
+ case "askHigh":
146818
+ return [];
146819
+ case "trust":
146820
+ return [{ action: "*", resource: "*", effect: "allow" }];
146821
+ default:
146822
+ return [];
146823
+ }
146824
+ }
146825
+ function presetGateMode(preset) {
146826
+ return preset === "readonly" || preset === "trust" ? "off" : "gate";
146827
+ }
146828
+ function presetAskActions(preset) {
146829
+ return ASK_ACTIONS[preset] ?? [];
146830
+ }
146831
+
146832
+ // src/feishu/wizard.ts
146833
+ var WIZARD_KEY_PREFIX = "feishu:v2:setup:";
146834
+ var STEPS = ["dir", "model", "perm", "confirm"];
146835
+ function wizardStart(title, anchorMessageId) {
146836
+ return {
146837
+ step: "dir",
146838
+ ...title && title.trim() ? { title: title.trim() } : {},
146839
+ ...anchorMessageId ? { anchorMessageId } : {}
146840
+ };
146841
+ }
146842
+ function reduceWizard(state, action) {
146843
+ switch (action.type) {
146844
+ case "start":
146845
+ return wizardStart(action.title, action.anchorMessageId);
146846
+ case "setDir":
146847
+ return state ? { ...state, dir: action.dir, step: "model" } : void 0;
146848
+ case "setModel":
146849
+ return state ? { ...state, model: action.model, step: "perm" } : void 0;
146850
+ case "setPerm":
146851
+ return state ? { ...state, perm: action.perm, step: "confirm" } : void 0;
146852
+ case "setPage":
146853
+ return state ? { ...state, page: Math.max(0, Math.floor(action.page)) } : void 0;
146854
+ case "cancel":
146855
+ return void 0;
146856
+ default:
146857
+ return state;
146858
+ }
146859
+ }
146860
+ function asString3(value3) {
146861
+ return typeof value3 === "string" ? value3 : "";
146862
+ }
146863
+ function parseModel(value3) {
146864
+ if (typeof value3 !== "object" || value3 === null) return void 0;
146865
+ const rec = value3;
146866
+ const providerID = asString3(rec.providerID);
146867
+ const id2 = asString3(rec.id);
146868
+ if (!providerID || !id2) return void 0;
146869
+ const name = asString3(rec.name);
146870
+ return { providerID, id: id2, ...name ? { name } : {} };
146871
+ }
146872
+ function parseWizardState(value3) {
146873
+ if (typeof value3 !== "object" || value3 === null) return void 0;
146874
+ const rec = value3;
146875
+ const step = asString3(rec.step);
146876
+ if (!STEPS.includes(step)) return void 0;
146877
+ const dir = asString3(rec.dir);
146878
+ const title = asString3(rec.title);
146879
+ const anchorMessageId = asString3(rec.anchorMessageId);
146880
+ const model = parseModel(rec.model);
146881
+ const perm = isPermissionPreset(rec.perm) ? rec.perm : void 0;
146882
+ const page = typeof rec.page === "number" && Number.isFinite(rec.page) ? Math.max(0, Math.floor(rec.page)) : void 0;
146883
+ return {
146884
+ step,
146885
+ ...dir ? { dir } : {},
146886
+ ...model ? { model } : {},
146887
+ ...perm ? { perm } : {},
146888
+ ...title ? { title } : {},
146889
+ ...page !== void 0 ? { page } : {},
146890
+ ...anchorMessageId ? { anchorMessageId } : {}
146891
+ };
146892
+ }
146893
+ var WizardStore = class {
146894
+ constructor(storage, log2) {
146895
+ this.storage = storage;
146896
+ this.log = log2;
146897
+ }
146898
+ storage;
146899
+ log;
146900
+ key(chatId) {
146901
+ return `${WIZARD_KEY_PREFIX}${chatId}`;
146902
+ }
146903
+ async get(chatId) {
146904
+ if (!chatId) return void 0;
146905
+ try {
146906
+ return parseWizardState(await this.storage.get(this.key(chatId)));
146907
+ } catch (err2) {
146908
+ this.log.warn("\u5411\u5BFC\u72B6\u6001\u8BFB\u53D6\u5931\u8D25", { chatId, error: errorMessage(err2) });
146909
+ return void 0;
146910
+ }
146911
+ }
146912
+ async set(chatId, state) {
146913
+ if (!chatId) return;
146914
+ try {
146915
+ if (!state) {
146916
+ await this.storage.remove(this.key(chatId));
146917
+ return;
146918
+ }
146919
+ await this.storage.set(this.key(chatId), state);
146920
+ } catch (err2) {
146921
+ this.log.warn("\u5411\u5BFC\u72B6\u6001\u5199\u5165\u5931\u8D25", { chatId, error: errorMessage(err2) });
146922
+ }
146923
+ }
146924
+ /** 应用一个动作并持久化;返回新状态(取消后为 undefined)。 */
146925
+ async apply(chatId, action) {
146926
+ const current = action.type === "start" ? void 0 : await this.get(chatId);
146927
+ const next = reduceWizard(current, action);
146928
+ await this.set(chatId, next);
146929
+ return next;
146930
+ }
146931
+ async start(chatId, title, anchorMessageId) {
146932
+ const state = wizardStart(title, anchorMessageId);
146933
+ await this.set(chatId, state);
146934
+ return state;
146935
+ }
146936
+ async cancel(chatId) {
146937
+ await this.set(chatId, void 0);
146938
+ }
146939
+ };
146940
+
146941
+ // src/feishu/recent.ts
146942
+ var RECENT_DIRS_KEY = "feishu:v2:recent:dirs";
146943
+ var RECENT_MODELS_KEY = "feishu:v2:recent:models";
146944
+ function isRecord5(value3) {
146945
+ return typeof value3 === "object" && value3 !== null && !Array.isArray(value3);
146946
+ }
146947
+ function modelKey(model) {
146948
+ return `${model.providerID}/${model.id}`;
146949
+ }
146950
+ function parseModel2(value3) {
146951
+ if (!isRecord5(value3)) return void 0;
146952
+ const providerID = typeof value3.providerID === "string" ? value3.providerID : "";
146953
+ const id2 = typeof value3.id === "string" ? value3.id : "";
146954
+ if (!providerID || !id2) return void 0;
146955
+ const name = typeof value3.name === "string" ? value3.name : "";
146956
+ return { providerID, id: id2, ...name ? { name } : {} };
146957
+ }
146958
+ var RecentStore = class {
146959
+ constructor(storage, log2, limits) {
146960
+ this.storage = storage;
146961
+ this.log = log2;
146962
+ this.limits = limits;
146963
+ }
146964
+ storage;
146965
+ log;
146966
+ limits;
146967
+ async listDirs() {
146968
+ const raw = await this.read(RECENT_DIRS_KEY);
146969
+ return Array.isArray(raw) ? raw.filter((v) => typeof v === "string") : [];
146970
+ }
146971
+ async addDir(dir) {
146972
+ const value3 = dir.trim();
146973
+ if (!value3) return;
146974
+ const list = [value3, ...(await this.listDirs()).filter((d) => d !== value3)];
146975
+ await this.write(RECENT_DIRS_KEY, list.slice(0, Math.max(1, this.limits.dirs)));
146976
+ }
146977
+ async listModels() {
146978
+ const raw = await this.read(RECENT_MODELS_KEY);
146979
+ if (!Array.isArray(raw)) return [];
146980
+ return raw.map(parseModel2).filter((m) => Boolean(m));
146981
+ }
146982
+ async addModel(model) {
146983
+ if (!model.providerID || !model.id) return;
146984
+ const key = modelKey(model);
146985
+ const list = [model, ...(await this.listModels()).filter((m) => modelKey(m) !== key)];
146986
+ await this.write(RECENT_MODELS_KEY, list.slice(0, Math.max(1, this.limits.models)));
146987
+ }
146988
+ async read(key) {
146989
+ try {
146990
+ return await this.storage.get(key);
146991
+ } catch (err2) {
146992
+ this.log.warn("\u6700\u8FD1\u8BB0\u5F55\u8BFB\u53D6\u5931\u8D25", { key, error: errorMessage(err2) });
146993
+ return void 0;
146994
+ }
146995
+ }
146996
+ async write(key, value3) {
146997
+ try {
146998
+ await this.storage.set(key, value3);
146999
+ } catch (err2) {
147000
+ this.log.warn("\u6700\u8FD1\u8BB0\u5F55\u5199\u5165\u5931\u8D25", { key, error: errorMessage(err2) });
147001
+ }
147002
+ }
147003
+ };
147004
+
147005
+ // src/feishu/models.ts
147006
+ function isRecord6(value3) {
147007
+ return typeof value3 === "object" && value3 !== null && !Array.isArray(value3);
147008
+ }
147009
+ function str3(value3) {
147010
+ return typeof value3 === "string" ? value3 : "";
147011
+ }
147012
+ function normalizeModelList(raw) {
147013
+ const arr = Array.isArray(raw) ? raw : isRecord6(raw) && Array.isArray(raw.data) ? raw.data : [];
147014
+ const out = [];
147015
+ const seen = /* @__PURE__ */ new Set();
147016
+ for (const item of arr) {
147017
+ if (!isRecord6(item)) continue;
147018
+ if (item.enabled === false) continue;
147019
+ const providerID = str3(item.providerID);
147020
+ const id2 = str3(item.id) || str3(item.modelID);
147021
+ if (!providerID || !id2) continue;
147022
+ const key = `${providerID}/${id2}`;
147023
+ if (seen.has(key)) continue;
147024
+ seen.add(key);
147025
+ const time = isRecord6(item.time) ? item.time : {};
147026
+ const released = typeof time.released === "number" && Number.isFinite(time.released) ? time.released : void 0;
147027
+ const status = str3(item.status) || void 0;
147028
+ out.push({
147029
+ providerID,
147030
+ id: id2,
147031
+ name: str3(item.name) || id2,
147032
+ ...released !== void 0 ? { released } : {},
147033
+ ...status ? { status } : {}
147034
+ });
147035
+ }
147036
+ return out;
147037
+ }
147038
+ function modelLabel(model) {
147039
+ return model.name && model.name.trim() ? model.name.trim() : `${model.providerID}/${model.id}`;
147040
+ }
147041
+ function sameModel(a, b) {
147042
+ if (!a || !b) return false;
147043
+ return a.providerID === b.providerID && a.id === b.id;
147044
+ }
147045
+ function extractSessionModel(raw) {
147046
+ if (!isRecord6(raw)) return void 0;
147047
+ const candidate = isRecord6(raw.model) ? raw.model : raw;
147048
+ const providerID = str3(candidate.providerID);
147049
+ const id2 = str3(candidate.id);
147050
+ if (!providerID || !id2) return void 0;
147051
+ return { providerID, id: id2 };
147052
+ }
147053
+ function matchesQuery(model, query) {
147054
+ const haystack = `${model.name} ${model.id} ${model.providerID} ${model.providerID}/${model.id}`.toLowerCase();
147055
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
147056
+ return tokens.length > 0 && tokens.every((t) => haystack.includes(t));
147057
+ }
147058
+ function sortModels(models) {
147059
+ return [...models].sort((a, b) => {
147060
+ const ar = a.released ?? 0;
147061
+ const br = b.released ?? 0;
147062
+ if (ar !== br) return br - ar;
147063
+ return a.name.localeCompare(b.name);
147064
+ });
147065
+ }
147066
+ function matchModel(query, models) {
147067
+ const q = query.trim();
147068
+ if (!q) return { ok: false, reason: "empty", candidates: [] };
147069
+ const lower = q.toLowerCase();
147070
+ const byRef = models.find((m) => `${m.providerID}/${m.id}`.toLowerCase() === lower);
147071
+ if (byRef) return { ok: true, model: byRef };
147072
+ const byId = models.filter((m) => m.id.toLowerCase() === lower);
147073
+ if (byId.length === 1) return { ok: true, model: byId[0] };
147074
+ if (byId.length > 1) return { ok: false, reason: "ambiguous", candidates: sortModels(byId).slice(0, 5) };
147075
+ const byName = models.filter((m) => m.name.toLowerCase() === lower);
147076
+ if (byName.length === 1) return { ok: true, model: byName[0] };
147077
+ if (byName.length > 1) return { ok: false, reason: "ambiguous", candidates: sortModels(byName).slice(0, 5) };
147078
+ const fuzzy = models.filter((m) => matchesQuery(m, q));
147079
+ if (fuzzy.length === 1) return { ok: true, model: fuzzy[0] };
147080
+ if (fuzzy.length > 1) return { ok: false, reason: "ambiguous", candidates: sortModels(fuzzy).slice(0, 5) };
147081
+ return { ok: false, reason: "not_found", candidates: [] };
147082
+ }
147083
+ function modelMatchErrorText(reason, candidates) {
147084
+ switch (reason) {
147085
+ case "empty":
147086
+ return "\u7528\u6CD5\uFF1A`/model <\u5173\u952E\u8BCD>`\uFF0C\u4F8B\u5982 `/model claude`\u3002";
147087
+ case "ambiguous":
147088
+ return [
147089
+ "\u5339\u914D\u5230\u591A\u4E2A\u6A21\u578B\uFF0C\u8BF7\u8F93\u5165\u66F4\u7CBE\u786E\u7684\u5173\u952E\u8BCD\uFF1A",
147090
+ ...candidates.map((m) => `- ${modelLabel(m)}\uFF08\`${m.providerID}/${m.id}\`\uFF09`)
147091
+ ].join("\n");
147092
+ case "not_found":
147093
+ default:
147094
+ return "\u6CA1\u6709\u5339\u914D\u7684\u6A21\u578B\uFF0C\u8BD5\u8BD5\u66F4\u77ED\u7684\u5173\u952E\u8BCD\u6216\u5148\u53D1\u9001 `/model` \u67E5\u770B\u5217\u8868\u3002";
147095
+ }
147096
+ }
146693
147097
 
146694
147098
  // src/feishu/session-list.ts
146695
147099
  function normalizeSessionList(raw) {
@@ -146706,10 +147110,10 @@ function normalizeSessionList(raw) {
146706
147110
  return sortByUpdatedDesc(out);
146707
147111
  }
146708
147112
  function normalizeSessionInfo(raw) {
146709
- if (!isRecord5(raw)) return void 0;
146710
- const sessionID = str3(raw.id) || str3(raw.sessionID);
147113
+ if (!isRecord7(raw)) return void 0;
147114
+ const sessionID = str4(raw.id) || str4(raw.sessionID);
146711
147115
  if (!sessionID) return void 0;
146712
- const title = str3(raw.title) || str3(raw.slug);
147116
+ const title = str4(raw.title) || str4(raw.slug);
146713
147117
  const updatedAt = extractUpdatedAt(raw);
146714
147118
  const directory = extractDirectory(raw);
146715
147119
  return {
@@ -146741,7 +147145,7 @@ function toMillis2(value3) {
146741
147145
  const ms = value3.getTime();
146742
147146
  return Number.isFinite(ms) && ms > 0 ? ms : 0;
146743
147147
  }
146744
- if (isRecord5(value3)) {
147148
+ if (isRecord7(value3)) {
146745
147149
  for (const key of ["epochMillis", "epochMilliseconds", "millis", "value"]) {
146746
147150
  const ms = toMillis2(value3[key]);
146747
147151
  if (ms > 0) return ms;
@@ -146784,7 +147188,7 @@ function directoryTail(dir) {
146784
147188
  }
146785
147189
  function extractArray(raw) {
146786
147190
  if (Array.isArray(raw)) return raw;
146787
- if (isRecord5(raw)) {
147191
+ if (isRecord7(raw)) {
146788
147192
  for (const key of ["data", "sessions", "items"]) {
146789
147193
  if (Array.isArray(raw[key])) return raw[key];
146790
147194
  }
@@ -146792,7 +147196,7 @@ function extractArray(raw) {
146792
147196
  return void 0;
146793
147197
  }
146794
147198
  function extractUpdatedAt(raw) {
146795
- const time = isRecord5(raw.time) ? raw.time : void 0;
147199
+ const time = isRecord7(raw.time) ? raw.time : void 0;
146796
147200
  const candidates = [
146797
147201
  time?.updated,
146798
147202
  raw.updatedAt,
@@ -146808,22 +147212,226 @@ function extractUpdatedAt(raw) {
146808
147212
  return 0;
146809
147213
  }
146810
147214
  function extractDirectory(raw) {
146811
- if (isRecord5(raw.location)) {
146812
- const directory = str3(raw.location.directory);
147215
+ if (isRecord7(raw.location)) {
147216
+ const directory = str4(raw.location.directory);
146813
147217
  if (directory) return directory;
146814
147218
  }
146815
- return str3(raw.directory) || (typeof raw.location === "string" ? raw.location : "");
147219
+ return str4(raw.directory) || (typeof raw.location === "string" ? raw.location : "");
146816
147220
  }
146817
147221
  function pad2(value3) {
146818
147222
  return value3 < 10 ? `0${value3}` : String(value3);
146819
147223
  }
146820
- function isRecord5(value3) {
147224
+ function isRecord7(value3) {
146821
147225
  return typeof value3 === "object" && value3 !== null && !Array.isArray(value3);
146822
147226
  }
146823
- function str3(value3) {
147227
+ function str4(value3) {
146824
147228
  return typeof value3 === "string" ? value3 : "";
146825
147229
  }
146826
147230
 
147231
+ // src/feishu/topic-guidance.ts
147232
+ var TOPIC_GUIDANCE_MARKER = "opencode-feishu-topic-guidance";
147233
+ function buildTopicGuidance(title) {
147234
+ return `\u672C\u4F1A\u8BDD\u4E3B\u9898\uFF1A\u300C${title}\u300D\u3002\u82E5\u7528\u6237\u660E\u663E\u8F6C\u5411\u4E0E\u8BE5\u4E3B\u9898\u65E0\u5173\u7684\u4EFB\u52A1\uFF0C\u53EF\u7B80\u77ED\u63D0\u9192\u4ED6\u7528 /new \u5F00\u65B0\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u56E0\u6B64\u62D2\u7EDD\u56DE\u7B54\uFF0C\u4E5F\u4E0D\u8981\u957F\u7BC7\u8BF4\u6559\u3002`;
147235
+ }
147236
+ async function injectTopicGuidance(input, deps) {
147237
+ try {
147238
+ if (!Array.isArray(input.system)) return;
147239
+ const system = input.system;
147240
+ if (system.some(isGuidancePart)) return;
147241
+ const link2 = await deps.resolveSession(input.sessionID);
147242
+ if (!link2) return;
147243
+ const title = (await deps.resolveTitle(input.sessionID, link2.chatId))?.trim();
147244
+ if (!title) return;
147245
+ system.push({
147246
+ type: "text",
147247
+ text: buildTopicGuidance(title),
147248
+ metadata: { [TOPIC_GUIDANCE_MARKER]: true }
147249
+ });
147250
+ } catch (err2) {
147251
+ deps.log.warn("\u4E3B\u9898\u8F6F\u5F15\u5BFC\u6CE8\u5165\u5931\u8D25", { sessionID: input.sessionID, error: errorMessage(err2) });
147252
+ }
147253
+ }
147254
+ function isGuidancePart(part) {
147255
+ if (typeof part !== "object" || part === null) return false;
147256
+ const metadata = part.metadata;
147257
+ if (typeof metadata !== "object" || metadata === null) return false;
147258
+ return metadata[TOPIC_GUIDANCE_MARKER] === true;
147259
+ }
147260
+
147261
+ // src/permission.ts
147262
+ function decideEffect(action, config) {
147263
+ if (config.permissionGate === "off" || config.permissionGate === "notify") {
147264
+ return {};
147265
+ }
147266
+ if (matchesAny(action, config.denyTools)) {
147267
+ return { effect: "deny", message: `feishu policy: ${action} \u5DF2\u5728 denyTools` };
147268
+ }
147269
+ if (matchesAny(action, config.allowTools)) {
147270
+ return { effect: "allow" };
147271
+ }
147272
+ if (config.permissionGate === "lockdown") {
147273
+ return { effect: "deny", message: `feishu lockdown: ${action} \u4E0D\u5728 allowTools` };
147274
+ }
147275
+ return { effect: "ask", message: `opencode-feishu-v2: \u9700\u8981\u4EBA\u5DE5\u6279\u51C6 ${action}` };
147276
+ }
147277
+ function decideEffectForSession(action, config, session) {
147278
+ if (!session) return decideEffect(action, config);
147279
+ if (session.gateMode === "off") return {};
147280
+ if (matchesAny(action, config.denyTools)) {
147281
+ return { effect: "deny", message: `feishu policy: ${action} \u5DF2\u5728 denyTools` };
147282
+ }
147283
+ if (matchesAny(action, config.allowTools)) {
147284
+ return { effect: "allow" };
147285
+ }
147286
+ if (session.askActions && matchesAny(action, session.askActions)) {
147287
+ return { effect: "ask", message: `opencode-feishu-v2: \u4F1A\u8BDD\u9884\u8BBE\u9700\u4EBA\u5DE5\u6279\u51C6 ${action}` };
147288
+ }
147289
+ return {};
147290
+ }
147291
+ function parseApprovalValue(rawValue) {
147292
+ if (typeof rawValue !== "object" || rawValue === null) return void 0;
147293
+ const record2 = rawValue;
147294
+ const token = typeof record2.t === "string" ? record2.t : "";
147295
+ const d = record2.d;
147296
+ if (!token) return void 0;
147297
+ if (d !== "once" && d !== "always" && d !== "reject") return void 0;
147298
+ return { token, decision: d };
147299
+ }
147300
+ var ApprovalManager = class {
147301
+ constructor(deps) {
147302
+ this.deps = deps;
147303
+ this.now = deps.now ?? (() => Date.now());
147304
+ }
147305
+ deps;
147306
+ seenRequests = new TtlMap(60 * 60 * 1e3);
147307
+ cards = new TtlMap(60 * 60 * 1e3);
147308
+ now;
147309
+ /** 处理 permission.asked:发审批卡。 */
147310
+ async onAsked(request3) {
147311
+ const requestID = request3.id;
147312
+ if (!requestID || !this.seenRequests.setIfAbsent(requestID, true)) return;
147313
+ const link2 = await this.deps.getLink(request3.sessionID);
147314
+ if (!link2) {
147315
+ this.deps.log.debug("permission.asked \u65E0\u98DE\u4E66\u6620\u5C04\uFF0C\u8DF3\u8FC7\u5BA1\u6279\u5361", {
147316
+ requestID,
147317
+ sessionID: request3.sessionID
147318
+ });
147319
+ return;
147320
+ }
147321
+ const canPersistAlways = Array.isArray(request3.save) && request3.save.length > 0;
147322
+ const token = this.deps.sign({
147323
+ requestID,
147324
+ sessionID: request3.sessionID,
147325
+ openId: link2.openId
147326
+ });
147327
+ const input = {
147328
+ requestID,
147329
+ sessionID: request3.sessionID,
147330
+ action: request3.action,
147331
+ resources: request3.resources ?? [],
147332
+ ...request3.message ? { message: request3.message } : {},
147333
+ canPersistAlways,
147334
+ token,
147335
+ maxResourcesShown: this.deps.config.maxResourcesShown
147336
+ };
147337
+ const result3 = link2.replyMessageId ? await this.deps.sender.replyCard(link2.replyMessageId, buildApprovalCard(input)) : await this.deps.sender.sendCard(link2.chatId, buildApprovalCard(input));
147338
+ if (!result3.ok || !result3.messageId) {
147339
+ this.deps.log.warn("\u5BA1\u6279\u5361\u53D1\u9001\u5931\u8D25", { requestID, error: result3.error ?? "unknown" });
147340
+ return;
147341
+ }
147342
+ this.cards.set(requestID, {
147343
+ messageId: result3.messageId,
147344
+ input,
147345
+ ...link2.dir ? { directory: link2.dir } : {},
147346
+ resolved: false
147347
+ });
147348
+ this.deps.log.info("\u5BA1\u6279\u5361\u5DF2\u53D1\u9001", {
147349
+ requestID,
147350
+ action: request3.action,
147351
+ canPersistAlways,
147352
+ hasDir: Boolean(link2.dir)
147353
+ });
147354
+ }
147355
+ /** 处理 permission.replied:把卡片收敛为结果态(若尚未由点击更新)。 */
147356
+ onReplied(event) {
147357
+ const tracked = this.cards.get(event.requestID);
147358
+ if (!tracked) return;
147359
+ if (!tracked.resolved) {
147360
+ const outcome = {
147361
+ reply: event.reply,
147362
+ operatorOpenId: "",
147363
+ at: this.now()
147364
+ };
147365
+ void this.patchResolved(tracked, outcome, event.requestID);
147366
+ }
147367
+ this.cards.delete(event.requestID);
147368
+ }
147369
+ /**
147370
+ * 处理卡片按钮点击。同步返回飞书回调响应(toast),reply 在后台完成。
147371
+ * 校验顺序:白名单 → 签名 → 绑定字段 → 防重放。
147372
+ */
147373
+ handleCardAction(action) {
147374
+ const parsed = parseApprovalValue(action.rawValue);
147375
+ if (!parsed) {
147376
+ return toast2("error", "\u65E0\u6CD5\u8BC6\u522B\u7684\u64CD\u4F5C");
147377
+ }
147378
+ if (!this.deps.isAllowed(action.operatorOpenId)) {
147379
+ this.deps.log.warn("\u62D2\u7EDD\u975E\u767D\u540D\u5355\u7528\u6237\u7684\u5BA1\u6279\u70B9\u51FB", {
147380
+ operator: action.operatorOpenId.slice(0, 8)
147381
+ });
147382
+ return toast2("error", "\u65E0\u5BA1\u6279\u6743\u9650");
147383
+ }
147384
+ const verified = this.deps.verify(parsed.token, { u: action.operatorOpenId });
147385
+ if (!verified.ok) {
147386
+ this.deps.log.warn("\u5BA1\u6279 token \u6821\u9A8C\u5931\u8D25", { reason: verified.reason });
147387
+ return toast2("error", `\u5BA1\u6279\u51ED\u8BC1\u65E0\u6548\uFF08${verified.reason}\uFF09`);
147388
+ }
147389
+ const claims = verified.claims;
147390
+ const ttl = Math.max(1e3, claims.e - this.now());
147391
+ if (!this.deps.replay.consume(claims.n, ttl)) {
147392
+ return toast2("warning", "\u8BE5\u64CD\u4F5C\u5DF2\u5904\u7406\uFF0C\u8BF7\u52FF\u91CD\u590D\u70B9\u51FB");
147393
+ }
147394
+ const tracked = this.cards.get(claims.r);
147395
+ const outcome = {
147396
+ reply: parsed.decision,
147397
+ operatorOpenId: action.operatorOpenId,
147398
+ at: this.now()
147399
+ };
147400
+ void this.applyReply(claims.r, claims.s, parsed.decision, tracked, outcome, tracked?.directory);
147401
+ return toast2(
147402
+ parsed.decision === "reject" ? "warning" : "success",
147403
+ parsed.decision === "reject" ? "\u5DF2\u62D2\u7EDD" : "\u5DF2\u5141\u8BB8"
147404
+ );
147405
+ }
147406
+ dispose() {
147407
+ this.seenRequests.clear();
147408
+ this.cards.clear();
147409
+ }
147410
+ async applyReply(requestID, sessionID, reply, tracked, outcome, directory) {
147411
+ try {
147412
+ await this.deps.reply({ sessionID, requestID, reply, ...directory ? { directory } : {} });
147413
+ } catch (err2) {
147414
+ this.deps.log.error("permission.reply \u5931\u8D25", {
147415
+ requestID,
147416
+ hasDir: Boolean(directory),
147417
+ error: errorMessage(err2)
147418
+ });
147419
+ return;
147420
+ }
147421
+ if (tracked && !tracked.resolved) {
147422
+ await this.patchResolved(tracked, outcome, requestID);
147423
+ }
147424
+ }
147425
+ async patchResolved(tracked, outcome, requestID) {
147426
+ tracked.resolved = true;
147427
+ const res = await this.deps.sender.patchCard(tracked.messageId, buildResolvedCard(tracked.input, outcome));
147428
+ if (!res.ok) this.deps.log.warn("\u5BA1\u6279\u7ED3\u679C\u5361\u7247\u66F4\u65B0\u5931\u8D25", { requestID, error: res.error ?? "unknown" });
147429
+ }
147430
+ };
147431
+ function toast2(type, content) {
147432
+ return { toast: { type, content } };
147433
+ }
147434
+
146827
147435
  // src/feishu/session-cards.ts
146828
147436
  function button2(text, type, value3) {
146829
147437
  return {
@@ -146994,158 +147602,6 @@ function parsePage(value3) {
146994
147602
  const parsed = typeof value3 === "number" ? value3 : typeof value3 === "string" ? Number.parseInt(value3, 10) : Number.NaN;
146995
147603
  return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
146996
147604
  }
146997
-
146998
- // src/feishu/perm-presets.ts
146999
- var SHELL_ACTIONS = ["shell", "bash"];
147000
- var PERMISSION_PRESETS = [
147001
- { id: "readonly", icon: "\u{1F512}", label: "\u53EA\u8BFB", description: "\u7981\u6B62\u7F16\u8F91 / \u6267\u884C / \u5199\u5165\uFF0C\u6700\u5B89\u5168\u3002" },
147002
- { id: "edit", icon: "\u270F\uFE0F", label: "\u53EF\u7F16\u8F91", description: "\u5141\u8BB8\u6539\u6587\u4EF6\uFF1B\u6267\u884C\u547D\u4EE4\u9700\u4F60\u5BA1\u6279\u3002" },
147003
- {
147004
- id: "askHigh",
147005
- icon: "\u26A0\uFE0F",
147006
- label: "\u9AD8\u98CE\u9669\u5BA1\u6279",
147007
- description: "\u7EE7\u627F\u9ED8\u8BA4\u89C4\u5219\uFF0C\u5BF9 shell / \u7F16\u8F91 / \u5916\u90E8\u76EE\u5F55\u9010\u6B21\u5BA1\u6279\u3002"
147008
- },
147009
- { id: "trust", icon: "\u{1F513}", label: "\u5B8C\u5168\u4FE1\u4EFB", description: "\u653E\u884C\u5168\u90E8\u64CD\u4F5C\uFF0C\u8BF7\u8C28\u614E\u4F7F\u7528\u3002" }
147010
- ];
147011
- var ASK_ACTIONS = {
147012
- readonly: [],
147013
- edit: ["shell", "bash"],
147014
- askHigh: ["shell", "bash", "edit", "external_directory"],
147015
- trust: []
147016
- };
147017
- var PRESET_IDS = PERMISSION_PRESETS.map((p) => p.id);
147018
- function isPermissionPreset(value3) {
147019
- return typeof value3 === "string" && PRESET_IDS.includes(value3);
147020
- }
147021
- function presetInfo(preset) {
147022
- return PERMISSION_PRESETS.find((p) => p.id === preset) ?? PERMISSION_PRESETS[1];
147023
- }
147024
- function presetLabel(preset) {
147025
- const info = presetInfo(preset);
147026
- return `${info.icon} ${info.label}`;
147027
- }
147028
- function presetToRuleset(preset) {
147029
- switch (preset) {
147030
- case "readonly": {
147031
- const actions = ["edit", "write", ...SHELL_ACTIONS];
147032
- return actions.map((action) => ({ action, resource: "*", effect: "deny" }));
147033
- }
147034
- case "edit": {
147035
- return [
147036
- { action: "edit", resource: "*", effect: "allow" },
147037
- ...SHELL_ACTIONS.map((action) => ({ action, resource: "*", effect: "ask" }))
147038
- ];
147039
- }
147040
- case "askHigh":
147041
- return [];
147042
- case "trust":
147043
- return [{ action: "*", resource: "*", effect: "allow" }];
147044
- default:
147045
- return [];
147046
- }
147047
- }
147048
- function presetGateMode(preset) {
147049
- return preset === "readonly" || preset === "trust" ? "off" : "gate";
147050
- }
147051
- function presetAskActions(preset) {
147052
- return ASK_ACTIONS[preset] ?? [];
147053
- }
147054
-
147055
- // src/feishu/models.ts
147056
- function isRecord6(value3) {
147057
- return typeof value3 === "object" && value3 !== null && !Array.isArray(value3);
147058
- }
147059
- function str4(value3) {
147060
- return typeof value3 === "string" ? value3 : "";
147061
- }
147062
- function normalizeModelList(raw) {
147063
- const arr = Array.isArray(raw) ? raw : isRecord6(raw) && Array.isArray(raw.data) ? raw.data : [];
147064
- const out = [];
147065
- const seen = /* @__PURE__ */ new Set();
147066
- for (const item of arr) {
147067
- if (!isRecord6(item)) continue;
147068
- if (item.enabled === false) continue;
147069
- const providerID = str4(item.providerID);
147070
- const id2 = str4(item.id) || str4(item.modelID);
147071
- if (!providerID || !id2) continue;
147072
- const key = `${providerID}/${id2}`;
147073
- if (seen.has(key)) continue;
147074
- seen.add(key);
147075
- const time = isRecord6(item.time) ? item.time : {};
147076
- const released = typeof time.released === "number" && Number.isFinite(time.released) ? time.released : void 0;
147077
- const status = str4(item.status) || void 0;
147078
- out.push({
147079
- providerID,
147080
- id: id2,
147081
- name: str4(item.name) || id2,
147082
- ...released !== void 0 ? { released } : {},
147083
- ...status ? { status } : {}
147084
- });
147085
- }
147086
- return out;
147087
- }
147088
- function modelLabel(model) {
147089
- return model.name && model.name.trim() ? model.name.trim() : `${model.providerID}/${model.id}`;
147090
- }
147091
- function sameModel(a, b) {
147092
- if (!a || !b) return false;
147093
- return a.providerID === b.providerID && a.id === b.id;
147094
- }
147095
- function extractSessionModel(raw) {
147096
- if (!isRecord6(raw)) return void 0;
147097
- const candidate = isRecord6(raw.model) ? raw.model : raw;
147098
- const providerID = str4(candidate.providerID);
147099
- const id2 = str4(candidate.id);
147100
- if (!providerID || !id2) return void 0;
147101
- return { providerID, id: id2 };
147102
- }
147103
- function matchesQuery(model, query) {
147104
- const haystack = `${model.name} ${model.id} ${model.providerID} ${model.providerID}/${model.id}`.toLowerCase();
147105
- const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
147106
- return tokens.length > 0 && tokens.every((t) => haystack.includes(t));
147107
- }
147108
- function sortModels(models) {
147109
- return [...models].sort((a, b) => {
147110
- const ar = a.released ?? 0;
147111
- const br = b.released ?? 0;
147112
- if (ar !== br) return br - ar;
147113
- return a.name.localeCompare(b.name);
147114
- });
147115
- }
147116
- function matchModel(query, models) {
147117
- const q = query.trim();
147118
- if (!q) return { ok: false, reason: "empty", candidates: [] };
147119
- const lower = q.toLowerCase();
147120
- const byRef = models.find((m) => `${m.providerID}/${m.id}`.toLowerCase() === lower);
147121
- if (byRef) return { ok: true, model: byRef };
147122
- const byId = models.filter((m) => m.id.toLowerCase() === lower);
147123
- if (byId.length === 1) return { ok: true, model: byId[0] };
147124
- if (byId.length > 1) return { ok: false, reason: "ambiguous", candidates: sortModels(byId).slice(0, 5) };
147125
- const byName = models.filter((m) => m.name.toLowerCase() === lower);
147126
- if (byName.length === 1) return { ok: true, model: byName[0] };
147127
- if (byName.length > 1) return { ok: false, reason: "ambiguous", candidates: sortModels(byName).slice(0, 5) };
147128
- const fuzzy = models.filter((m) => matchesQuery(m, q));
147129
- if (fuzzy.length === 1) return { ok: true, model: fuzzy[0] };
147130
- if (fuzzy.length > 1) return { ok: false, reason: "ambiguous", candidates: sortModels(fuzzy).slice(0, 5) };
147131
- return { ok: false, reason: "not_found", candidates: [] };
147132
- }
147133
- function modelMatchErrorText(reason, candidates) {
147134
- switch (reason) {
147135
- case "empty":
147136
- return "\u7528\u6CD5\uFF1A`/model <\u5173\u952E\u8BCD>`\uFF0C\u4F8B\u5982 `/model claude`\u3002";
147137
- case "ambiguous":
147138
- return [
147139
- "\u5339\u914D\u5230\u591A\u4E2A\u6A21\u578B\uFF0C\u8BF7\u8F93\u5165\u66F4\u7CBE\u786E\u7684\u5173\u952E\u8BCD\uFF1A",
147140
- ...candidates.map((m) => `- ${modelLabel(m)}\uFF08\`${m.providerID}/${m.id}\`\uFF09`)
147141
- ].join("\n");
147142
- case "not_found":
147143
- default:
147144
- return "\u6CA1\u6709\u5339\u914D\u7684\u6A21\u578B\uFF0C\u8BD5\u8BD5\u66F4\u77ED\u7684\u5173\u952E\u8BCD\u6216\u5148\u53D1\u9001 `/model` \u67E5\u770B\u5217\u8868\u3002";
147145
- }
147146
- }
147147
-
147148
- // src/feishu/setup-cards.ts
147149
147605
  var SETUP_FORM_CMD = "setup.form";
147150
147606
  var SETUP_FORM_NAME = "setup_form";
147151
147607
  var SETUP_FORM_FIELDS = {
@@ -147497,506 +147953,1039 @@ function shortenMiddle(text, max6) {
147497
147953
  const tail = keep - head3;
147498
147954
  return `${one.slice(0, head3)}\u2026${one.slice(one.length - tail)}`;
147499
147955
  }
147500
- var SYSTEM_DIRS = [
147501
- "/etc",
147502
- "/usr",
147503
- "/bin",
147504
- "/sbin",
147505
- "/lib",
147506
- "/lib64",
147507
- "/boot",
147508
- "/dev",
147509
- "/proc",
147510
- "/sys",
147511
- "/run",
147512
- "/root",
147513
- "/var"
147514
- ];
147515
- function isUnder(child, root) {
147516
- if (!root || root === "/") return true;
147517
- const normalizedRoot = root.endsWith(sep) ? root.slice(0, -1) : root;
147518
- return child === normalizedRoot || child.startsWith(`${normalizedRoot}${sep}`);
147956
+
147957
+ // src/session/context.ts
147958
+ function clampPageSize(value3) {
147959
+ const base2 = typeof value3 === "number" && Number.isFinite(value3) ? Math.floor(value3) : 8;
147960
+ return Math.min(20, Math.max(5, base2));
147519
147961
  }
147520
- function isSystemPath(path2) {
147521
- return SYSTEM_DIRS.some((dir) => isUnder(path2, dir));
147962
+ function toast3(type, content) {
147963
+ return { toast: { type, content } };
147522
147964
  }
147523
- function validateDirectory(rawPath, allowedRoots, deps = {}) {
147524
- const raw = rawPath.trim();
147525
- const roots = allowedRoots.map((r) => resolve$1(r)).filter((r) => r && r !== "");
147526
- if (roots.length === 0) {
147527
- return err("empty", "\u672A\u914D\u7F6E\u5141\u8BB8\u7684\u5DE5\u4F5C\u76EE\u5F55\u6839\uFF08`allowedRoots`\uFF09\uFF0C\u65E0\u6CD5\u786E\u5B9A\u9ED8\u8BA4\u76EE\u5F55\u3002");
147528
- }
147529
- let target;
147530
- if (!raw) {
147531
- target = roots[0];
147532
- } else {
147533
- if (!isAbsolute(raw)) return err("not_absolute", "\u76EE\u5F55\u5FC5\u987B\u662F**\u7EDD\u5BF9\u8DEF\u5F84**\uFF08\u4EE5 `/` \u5F00\u5934\uFF09\u3002");
147534
- target = resolve$1(raw);
147965
+ function createSessionContext(deps) {
147966
+ const now3 = deps.now ?? (() => Date.now());
147967
+ const ctx = {
147968
+ deps,
147969
+ now: now3,
147970
+ threadRouting: deps.threadRouting ?? true,
147971
+ modelPageSize: deps.modelPageSize ?? 8,
147972
+ sessionPageSize: clampPageSize(deps.sessionPageSize),
147973
+ async reply(message, text) {
147974
+ if (message.threadId) {
147975
+ await deps.sender.replyText(message.messageId, text);
147976
+ return;
147977
+ }
147978
+ await deps.sender.sendText(message.chatId, text);
147979
+ },
147980
+ async replyChat(chatId, text) {
147981
+ await deps.sender.sendText(chatId, text);
147982
+ },
147983
+ async patchCard(messageId, card) {
147984
+ if (!messageId) return;
147985
+ const res = await deps.sender.patchCard(messageId, card);
147986
+ if (!res.ok) deps.log.warn("\u5411\u5BFC\u5361\u7247\u66F4\u65B0\u5931\u8D25", { error: res.error ?? "unknown" });
147987
+ },
147988
+ async loadModels() {
147989
+ try {
147990
+ return [...await deps.listModels()];
147991
+ } catch (err2) {
147992
+ deps.log.warn("\u6A21\u578B\u5217\u8868\u83B7\u53D6\u5931\u8D25", { error: errorMessage(err2) });
147993
+ return [];
147994
+ }
147995
+ },
147996
+ recentModelsLimit() {
147997
+ return deps.recentModelsLimit ?? 5;
147998
+ },
147999
+ async threadSessionID(message) {
148000
+ if (!message.threadId) return void 0;
148001
+ const link2 = await deps.sessionMap.resolveByThread(message.threadId);
148002
+ return link2?.sessionID;
148003
+ },
148004
+ async scopeSessionID(message, scope3) {
148005
+ if (scope3 === "thread") return ctx.threadSessionID(message);
148006
+ const active = await deps.sessionMap.getActive(message.chatId);
148007
+ return active?.sessionID;
148008
+ }
148009
+ };
148010
+ return ctx;
148011
+ }
148012
+ var MAX_ROOT_SUBDIRS = 15;
148013
+ async function scanRootSubdirs(root, deps = {}) {
148014
+ const rootPath = root.trim();
148015
+ if (!rootPath) return [];
148016
+ const rd = deps.readdir ?? ((path2, options2) => readdir(path2, options2));
148017
+ let entries3;
148018
+ try {
148019
+ entries3 = await rd(rootPath, { withFileTypes: true });
148020
+ } catch {
148021
+ return [];
147535
148022
  }
147536
- const stat = deps.statSync ?? statSync;
147537
- const realpath = deps.realpathSync ?? realpathSync;
147538
- const mkdir = deps.mkdirSync ?? mkdirSync;
147539
- const blocked = forbiddenReason(target);
147540
- if (blocked) return blocked;
147541
- if (!roots.some((root) => isUnder(target, root))) {
147542
- return err("outside_allowed", outsideMessage(roots));
148023
+ const names = entries3.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).slice(0, MAX_ROOT_SUBDIRS);
148024
+ const out = [];
148025
+ for (const name of names) {
148026
+ const path2 = join(rootPath, name);
148027
+ out.push({ name, path: path2, isRepo: await hasGit(path2, rd) });
147543
148028
  }
147544
- let exists2 = true;
148029
+ return out;
148030
+ }
148031
+ async function hasGit(path2, rd) {
147545
148032
  try {
147546
- if (!stat(target).isDirectory()) return err("not_dir", `\u4E0D\u662F\u76EE\u5F55\uFF1A\`${target}\``);
148033
+ const entries3 = await rd(path2, { withFileTypes: true });
148034
+ return entries3.some((e) => e.name === ".git");
147547
148035
  } catch {
147548
- exists2 = false;
148036
+ return false;
147549
148037
  }
147550
- if (!exists2) {
148038
+ }
148039
+
148040
+ // src/session/model-perm.ts
148041
+ async function currentModel(ctx, sessionID) {
148042
+ const link2 = await ctx.deps.sessionMap.resolveBySession(sessionID);
148043
+ if (ctx.deps.getSessionModel) {
147551
148044
  try {
147552
- mkdir(target, { recursive: true });
147553
- } catch {
147554
- return err("create_failed", `\u76EE\u5F55\u4E0D\u5B58\u5728\u4E14\u65E0\u6CD5\u521B\u5EFA\uFF1A\`${target}\`\u3002\u8BF7\u68C0\u67E5\u8DEF\u5F84\u4E0E\u6743\u9650\u3002`);
148045
+ const model = await ctx.deps.getSessionModel(sessionID, link2?.dir);
148046
+ if (model) return model;
148047
+ } catch (err2) {
148048
+ ctx.deps.log.warn("\u8BFB\u56DE\u4F1A\u8BDD\u6A21\u578B\u5931\u8D25\uFF0C\u56DE\u9000\u8BB0\u5F55\u503C", { sessionID, error: errorMessage(err2) });
147555
148049
  }
147556
148050
  }
147557
- let real = target;
148051
+ return link2?.model;
148052
+ }
148053
+ async function renderModelCard(ctx, state, pageOverride) {
148054
+ const models = await ctx.loadModels();
148055
+ const recent = await ctx.deps.recent.listModels();
148056
+ return buildModelCard({
148057
+ models,
148058
+ recent,
148059
+ ...state.model ? { current: state.model } : {},
148060
+ page: pageOverride ?? state.page ?? 0,
148061
+ pageSize: ctx.modelPageSize,
148062
+ recentLimit: ctx.recentModelsLimit()
148063
+ });
148064
+ }
148065
+ async function sendModelCardToThread(ctx, message, sessionID, page) {
148066
+ const current = await currentModel(ctx, sessionID);
148067
+ const models = await ctx.loadModels();
148068
+ const recent = await ctx.deps.recent.listModels();
148069
+ const card = buildModelCard({
148070
+ models,
148071
+ recent,
148072
+ ...current ? { current } : {},
148073
+ page,
148074
+ pageSize: ctx.modelPageSize,
148075
+ recentLimit: ctx.recentModelsLimit(),
148076
+ sid: sessionID
148077
+ });
148078
+ await ctx.deps.sender.replyCard(message.messageId, card);
148079
+ }
148080
+ async function switchModelInThread(ctx, message, args2) {
148081
+ const sessionID = await ctx.threadSessionID(message);
148082
+ if (!sessionID) {
148083
+ await ctx.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\u3002\u8BF7\u56DE\u5230\u4E3B\u804A\u5929\u6D41\u7528 `/new` \u65B0\u5EFA\u3002");
148084
+ return;
148085
+ }
148086
+ if (!args2.trim()) {
148087
+ await sendModelCardToThread(ctx, message, sessionID, 0);
148088
+ return;
148089
+ }
148090
+ const models = await ctx.loadModels();
148091
+ const matched = matchModel(args2, models);
148092
+ if (!matched.ok) {
148093
+ await ctx.reply(message, modelMatchErrorText(matched.reason, matched.candidates));
148094
+ return;
148095
+ }
147558
148096
  try {
147559
- real = realpath(target);
147560
- } catch {
147561
- real = target;
148097
+ const outcome = await ctx.deps.switchSessionModel(sessionID, matched.model);
148098
+ await ctx.deps.recent.addModel(matched.model);
148099
+ await ctx.reply(message, modelSwitchReply(outcome, matched.model));
148100
+ } catch (err2) {
148101
+ ctx.deps.log.warn("\u5207\u6362\u4F1A\u8BDD\u6A21\u578B\u5931\u8D25", { sessionID, error: errorMessage(err2) });
148102
+ await ctx.reply(message, `\u26A0\uFE0F \u5207\u6362\u6A21\u578B\u5931\u8D25\uFF1A${errorMessage(err2)}`);
147562
148103
  }
147563
- const blockedReal = forbiddenReason(real);
147564
- if (blockedReal) return blockedReal;
147565
- if (!roots.some((root) => isUnder(real, root))) {
147566
- return err("outside_allowed", outsideMessage(roots));
148104
+ }
148105
+ async function sendPermCardToThread(ctx, message, sessionID) {
148106
+ const link2 = await ctx.deps.sessionMap.resolveBySession(sessionID);
148107
+ const card = buildPermCard({ ...link2?.perm ? { current: link2.perm } : {}, sid: sessionID });
148108
+ await ctx.deps.sender.replyCard(message.messageId, card);
148109
+ }
148110
+ async function setPermInThread(ctx, message, args2) {
148111
+ const sessionID = await ctx.threadSessionID(message);
148112
+ if (!sessionID) {
148113
+ await ctx.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\u3002\u8BF7\u56DE\u5230\u4E3B\u804A\u5929\u6D41\u7528 `/new` \u65B0\u5EFA\u3002");
148114
+ return;
147567
148115
  }
147568
- return { ok: true, path: real };
148116
+ const arg = args2.trim();
148117
+ if (!arg) {
148118
+ await sendPermCardToThread(ctx, message, sessionID);
148119
+ return;
148120
+ }
148121
+ if (!isPermissionPreset(arg)) {
148122
+ await ctx.reply(message, permUsageText());
148123
+ return;
148124
+ }
148125
+ await ctx.deps.applyPermissionPreset(sessionID, arg);
148126
+ await ctx.reply(message, `\u2705 \u5DF2\u66F4\u65B0\u672C\u4F1A\u8BDD\u6743\u9650\uFF1A${presetLabel(arg)}`);
147569
148127
  }
147570
- function outsideMessage(roots) {
147571
- return `\u76EE\u5F55\u4E0D\u5728\u5141\u8BB8\u8303\u56F4\u5185\u3002\u5141\u8BB8\u7684\u6839\u76EE\u5F55\uFF1A${roots.map((r) => `\`${r}\``).join("\u3001")}\u3002`;
148128
+ function permUsageText() {
148129
+ const list = PERMISSION_PRESETS.map((p) => `\`${p.id}\`\uFF08${presetInfo(p.id).icon}${p.label}\uFF09`).join("\u3001");
148130
+ return `\u672A\u77E5\u6743\u9650\u6863\u4F4D\u3002\u53EF\u7528\uFF1A${list}\u3002
148131
+ \u4F8B\u5982\uFF1A\`/perm edit\`\u3002`;
147572
148132
  }
147573
- function forbiddenReason(path2) {
147574
- if (path2 === sep) return err("forbidden", "\u4E0D\u80FD\u4F7F\u7528\u6839\u76EE\u5F55 `/`\uFF0C\u8BF7\u9009\u62E9\u5177\u4F53\u7684\u9879\u76EE\u5B50\u76EE\u5F55\u3002");
147575
- if (isSystemPath(path2)) return err("system", `\u7CFB\u7EDF\u76EE\u5F55\u4E0D\u53EF\u4F5C\u4E3A\u5DE5\u4F5C\u76EE\u5F55\uFF1A\`${path2}\`\u3002`);
147576
- return void 0;
148133
+ function modelSwitchReply(outcome, requested) {
148134
+ const effective = outcome?.effective ?? requested;
148135
+ const ref = `**${modelLabel(effective)}**
148136
+ \`${effective.providerID}/${effective.id}\``;
148137
+ if (outcome?.mismatch) {
148138
+ return [
148139
+ `\u26A0\uFE0F \u6A21\u578B\u53EF\u80FD\u672A\u751F\u6548\uFF1A\u8BF7\u6C42 **${modelLabel(outcome.requested)}**\uFF0C\u670D\u52A1\u7AEF\u5B9E\u9645\u4E3A ${ref}\u3002`,
148140
+ "\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\uFF1B\u5386\u53F2\u6D88\u606F\u4ECD\u662F\u65E7\u6A21\u578B\uFF0C\u5C5E\u6B63\u5E38\u3002"
148141
+ ].join("\n");
148142
+ }
148143
+ if (outcome && !outcome.verified) {
148144
+ return `\u2705 \u5DF2\u8BF7\u6C42\u5207\u6362\u6A21\u578B\uFF1A${ref}
148145
+ \u672A\u80FD\u8BFB\u56DE\u6821\u9A8C${outcome.warning ? `\uFF08${outcome.warning}\uFF09` : ""}\uFF1B\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\u3002`;
148146
+ }
148147
+ return `\u2705 \u5DF2\u5207\u6362\u6A21\u578B\uFF1A${ref}
148148
+ \u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\uFF1B\u8BE5\u4F1A\u8BDD\u6B64\u524D\u7684\u5386\u53F2\u6D88\u606F\u4ECD\u663E\u793A\u65E7\u6A21\u578B\uFF0C\u5C5E\u6B63\u5E38\u3002`;
147577
148149
  }
147578
- function err(reason, message) {
147579
- return { ok: false, reason, message };
148150
+ function modelSwitchDoneCard(outcome, requested) {
148151
+ const effective = outcome?.effective ?? requested;
148152
+ if (outcome?.mismatch) {
148153
+ return buildSetupDoneCard("\u26A0\uFE0F \u6A21\u578B\u53EF\u80FD\u672A\u751F\u6548", [
148154
+ `\u8BF7\u6C42\uFF1A**${modelLabel(outcome.requested)}**`,
148155
+ `\u5B9E\u9645\uFF1A**${modelLabel(effective)}**`,
148156
+ "\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\uFF1B\u5386\u53F2\u6D88\u606F\u4ECD\u662F\u65E7\u6A21\u578B\uFF0C\u5C5E\u6B63\u5E38\u3002"
148157
+ ]);
148158
+ }
148159
+ if (outcome && !outcome.verified) {
148160
+ return buildSetupDoneCard("\u2705 \u5DF2\u5207\u6362\u6A21\u578B", [
148161
+ `\u5F53\u524D\u6A21\u578B\uFF1A**${modelLabel(effective)}**`,
148162
+ "\uFF08\u672A\u80FD\u8BFB\u56DE\u6821\u9A8C\uFF1B\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\u3002\uFF09"
148163
+ ]);
148164
+ }
148165
+ return buildSetupDoneCard("\u2705 \u5DF2\u5207\u6362\u6A21\u578B", [
148166
+ `\u5F53\u524D\u6A21\u578B\uFF1A**${modelLabel(effective)}**`,
148167
+ "\uFF08\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\uFF1B\u5386\u53F2\u6D88\u606F\u4ECD\u662F\u65E7\u6A21\u578B\uFF0C\u5C5E\u6B63\u5E38\u3002\uFF09"
148168
+ ]);
147580
148169
  }
147581
-
147582
- // src/feishu/wizard.ts
147583
- var WIZARD_KEY_PREFIX = "feishu:v2:setup:";
147584
- var STEPS = ["dir", "model", "perm", "confirm"];
147585
- function wizardStart(title, anchorMessageId) {
148170
+ function createModelPermApi(ctx) {
147586
148171
  return {
147587
- step: "dir",
147588
- ...title && title.trim() ? { title: title.trim() } : {},
147589
- ...anchorMessageId ? { anchorMessageId } : {}
148172
+ currentModel: (sessionID) => currentModel(ctx, sessionID),
148173
+ renderModelCard: (state, pageOverride) => renderModelCard(ctx, state, pageOverride),
148174
+ sendModelCardToThread: (message, sessionID, page) => sendModelCardToThread(ctx, message, sessionID, page),
148175
+ switchModelInThread: (message, args2) => switchModelInThread(ctx, message, args2),
148176
+ sendPermCardToThread: (message, sessionID) => sendPermCardToThread(ctx, message, sessionID),
148177
+ setPermInThread: (message, args2) => setPermInThread(ctx, message, args2)
147590
148178
  };
147591
148179
  }
147592
- function reduceWizard(state, action) {
147593
- switch (action.type) {
147594
- case "start":
147595
- return wizardStart(action.title, action.anchorMessageId);
147596
- case "setDir":
147597
- return state ? { ...state, dir: action.dir, step: "model" } : void 0;
147598
- case "setModel":
147599
- return state ? { ...state, model: action.model, step: "perm" } : void 0;
147600
- case "setPerm":
147601
- return state ? { ...state, perm: action.perm, step: "confirm" } : void 0;
147602
- case "setPage":
147603
- return state ? { ...state, page: Math.max(0, Math.floor(action.page)) } : void 0;
147604
- case "cancel":
147605
- return void 0;
147606
- default:
147607
- return state;
148180
+
148181
+ // src/session/setup-wizard.ts
148182
+ async function cmdNew(ctx, message, args2) {
148183
+ const title = args2.trim() || void 0;
148184
+ if (!ctx.threadRouting) {
148185
+ const finalTitle = title ?? defaultSessionTitle(ctx.now());
148186
+ const created = await ctx.deps.createSession({
148187
+ title: finalTitle,
148188
+ chatId: message.chatId,
148189
+ openId: message.senderOpenId
148190
+ });
148191
+ await ctx.reply(message, `\u2705 \u5DF2\u65B0\u5EFA\u5E76\u5207\u6362\u5230\u4F1A\u8BDD\u300C${finalTitle}\u300D
148192
+ \`${created.id}\``);
148193
+ return;
147608
148194
  }
148195
+ await openSetupForm(ctx, message, title);
147609
148196
  }
147610
- function asString3(value3) {
147611
- return typeof value3 === "string" ? value3 : "";
148197
+ async function cmdForm(ctx, message, args2) {
148198
+ await openSetupForm(ctx, message, args2.trim() || void 0);
147612
148199
  }
147613
- function parseModel(value3) {
147614
- if (typeof value3 !== "object" || value3 === null) return void 0;
147615
- const rec = value3;
147616
- const providerID = asString3(rec.providerID);
147617
- const id2 = asString3(rec.id);
147618
- if (!providerID || !id2) return void 0;
147619
- const name = asString3(rec.name);
147620
- return { providerID, id: id2, ...name ? { name } : {} };
148200
+ async function openSetupForm(ctx, message, title) {
148201
+ if (message.threadId) {
148202
+ await ctx.reply(message, threadForbiddenText("form"));
148203
+ return;
148204
+ }
148205
+ if (!ctx.threadRouting) {
148206
+ await ctx.reply(message, "\u5F53\u524D\u4E3A\u56DE\u9000\u6A21\u5F0F\uFF08`threadRouting=false`\uFF09\uFF0C\u4E0D\u652F\u6301\u8868\u5355\u5EFA\u4F1A\u8BDD\uFF0C\u8BF7\u7528 `/new`\u3002");
148207
+ return;
148208
+ }
148209
+ let state = await ctx.deps.wizard.get(message.chatId);
148210
+ if (!state) {
148211
+ state = await ctx.deps.wizard.start(message.chatId, title, message.messageId);
148212
+ } else if (title !== void 0) {
148213
+ state = { ...state, title };
148214
+ await ctx.deps.wizard.set(message.chatId, state);
148215
+ }
148216
+ const card = await renderFormCard(ctx, state);
148217
+ const res = await ctx.deps.sender.sendCard(message.chatId, card);
148218
+ if (!res.ok) ctx.deps.log.warn("\u8868\u5355\u5361\u53D1\u9001\u5931\u8D25", { chatId: message.chatId, error: res.error ?? "unknown" });
147621
148219
  }
147622
- function parseWizardState(value3) {
147623
- if (typeof value3 !== "object" || value3 === null) return void 0;
147624
- const rec = value3;
147625
- const step = asString3(rec.step);
147626
- if (!STEPS.includes(step)) return void 0;
147627
- const dir = asString3(rec.dir);
147628
- const title = asString3(rec.title);
147629
- const anchorMessageId = asString3(rec.anchorMessageId);
147630
- const model = parseModel(rec.model);
147631
- const perm = isPermissionPreset(rec.perm) ? rec.perm : void 0;
147632
- const page = typeof rec.page === "number" && Number.isFinite(rec.page) ? Math.max(0, Math.floor(rec.page)) : void 0;
147633
- return {
147634
- step,
147635
- ...dir ? { dir } : {},
147636
- ...model ? { model } : {},
147637
- ...perm ? { perm } : {},
147638
- ...title ? { title } : {},
147639
- ...page !== void 0 ? { page } : {},
147640
- ...anchorMessageId ? { anchorMessageId } : {}
147641
- };
148220
+ async function cmdDir(ctx, message, args2) {
148221
+ if (message.threadId) {
148222
+ await ctx.reply(message, threadForbiddenText("dir"));
148223
+ return;
148224
+ }
148225
+ const validation = ctx.deps.validateDir(args2);
148226
+ if (!validation.ok) {
148227
+ await ctx.reply(message, validation.message);
148228
+ return;
148229
+ }
148230
+ if (!await ctx.deps.wizard.get(message.chatId)) {
148231
+ await ctx.deps.wizard.start(message.chatId, void 0, message.messageId);
148232
+ }
148233
+ const state = await ctx.deps.wizard.apply(message.chatId, { type: "setDir", dir: validation.path });
148234
+ await ctx.deps.recent.addDir(validation.path);
148235
+ if (!state) {
148236
+ await ctx.reply(message, "\u5411\u5BFC\u72B6\u6001\u5DF2\u4E22\u5931\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 `/new` \u5F00\u59CB\u3002");
148237
+ return;
148238
+ }
148239
+ await ctx.deps.sender.sendCard(message.chatId, await renderFormCard(ctx, state));
147642
148240
  }
147643
- var WizardStore = class {
147644
- constructor(storage, log2) {
147645
- this.storage = storage;
147646
- this.log = log2;
148241
+ async function cmdModel(ctx, message, args2, scope3) {
148242
+ if (scope3 === "thread") {
148243
+ await switchModelInThread(ctx, message, args2);
148244
+ return;
147647
148245
  }
147648
- storage;
147649
- log;
147650
- key(chatId) {
147651
- return `${WIZARD_KEY_PREFIX}${chatId}`;
148246
+ if (!await ctx.deps.wizard.get(message.chatId)) {
148247
+ await ctx.deps.wizard.start(message.chatId, void 0, message.messageId);
147652
148248
  }
147653
- async get(chatId) {
147654
- if (!chatId) return void 0;
147655
- try {
147656
- return parseWizardState(await this.storage.get(this.key(chatId)));
147657
- } catch (err2) {
147658
- this.log.warn("\u5411\u5BFC\u72B6\u6001\u8BFB\u53D6\u5931\u8D25", { chatId, error: errorMessage(err2) });
147659
- return void 0;
147660
- }
148249
+ if (!args2.trim()) {
148250
+ const state = await ctx.deps.wizard.get(message.chatId);
148251
+ await ctx.deps.sender.sendCard(message.chatId, await renderFormCard(ctx, state));
148252
+ return;
147661
148253
  }
147662
- async set(chatId, state) {
147663
- if (!chatId) return;
147664
- try {
147665
- if (!state) {
147666
- await this.storage.remove(this.key(chatId));
147667
- return;
147668
- }
147669
- await this.storage.set(this.key(chatId), state);
147670
- } catch (err2) {
147671
- this.log.warn("\u5411\u5BFC\u72B6\u6001\u5199\u5165\u5931\u8D25", { chatId, error: errorMessage(err2) });
147672
- }
148254
+ const models = await ctx.loadModels();
148255
+ const matched = matchModel(args2, models);
148256
+ if (!matched.ok) {
148257
+ await ctx.reply(message, modelMatchErrorText(matched.reason, matched.candidates));
148258
+ return;
147673
148259
  }
147674
- /** 应用一个动作并持久化;返回新状态(取消后为 undefined)。 */
147675
- async apply(chatId, action) {
147676
- const current = action.type === "start" ? void 0 : await this.get(chatId);
147677
- const next = reduceWizard(current, action);
147678
- await this.set(chatId, next);
147679
- return next;
148260
+ const next = await ctx.deps.wizard.apply(message.chatId, { type: "setModel", model: matched.model });
148261
+ await ctx.deps.recent.addModel(matched.model);
148262
+ if (!next) return;
148263
+ await ctx.deps.sender.sendCard(message.chatId, await renderFormCard(ctx, next));
148264
+ }
148265
+ async function cmdPerm(ctx, message, args2, scope3) {
148266
+ if (scope3 === "thread") {
148267
+ await setPermInThread(ctx, message, args2);
148268
+ return;
147680
148269
  }
147681
- async start(chatId, title, anchorMessageId) {
147682
- const state = wizardStart(title, anchorMessageId);
147683
- await this.set(chatId, state);
147684
- return state;
148270
+ const arg = args2.trim();
148271
+ if (!await ctx.deps.wizard.get(message.chatId)) {
148272
+ await ctx.deps.wizard.start(message.chatId, void 0, message.messageId);
147685
148273
  }
147686
- async cancel(chatId) {
147687
- await this.set(chatId, void 0);
148274
+ if (!arg) {
148275
+ const state = await ctx.deps.wizard.get(message.chatId);
148276
+ await ctx.deps.sender.sendCard(message.chatId, await renderFormCard(ctx, state));
148277
+ return;
147688
148278
  }
147689
- };
147690
-
147691
- // src/feishu/recent.ts
147692
- var RECENT_DIRS_KEY = "feishu:v2:recent:dirs";
147693
- var RECENT_MODELS_KEY = "feishu:v2:recent:models";
147694
- function isRecord7(value3) {
147695
- return typeof value3 === "object" && value3 !== null && !Array.isArray(value3);
148279
+ if (!isPermissionPreset(arg)) {
148280
+ await ctx.reply(message, permUsageText());
148281
+ return;
148282
+ }
148283
+ const next = await ctx.deps.wizard.apply(message.chatId, { type: "setPerm", perm: arg });
148284
+ if (!next) return;
148285
+ await ctx.deps.sender.sendCard(message.chatId, await renderFormCard(ctx, next));
147696
148286
  }
147697
- function modelKey(model) {
147698
- return `${model.providerID}/${model.id}`;
148287
+ async function cmdCancel(ctx, message) {
148288
+ if (message.threadId) {
148289
+ await ctx.reply(message, threadForbiddenText("cancel"));
148290
+ return;
148291
+ }
148292
+ await ctx.deps.wizard.cancel(message.chatId);
148293
+ await ctx.reply(message, "\u2716\uFE0F \u5DF2\u53D6\u6D88\u5EFA\u4F1A\u8BDD\u8868\u5355\u3002\u53D1\u9001 `/new [\u6807\u9898]` \u53EF\u91CD\u65B0\u5F00\u59CB\u3002");
147699
148294
  }
147700
- function parseModel2(value3) {
147701
- if (!isRecord7(value3)) return void 0;
147702
- const providerID = typeof value3.providerID === "string" ? value3.providerID : "";
147703
- const id2 = typeof value3.id === "string" ? value3.id : "";
147704
- if (!providerID || !id2) return void 0;
147705
- const name = typeof value3.name === "string" ? value3.name : "";
147706
- return { providerID, id: id2, ...name ? { name } : {} };
148295
+ async function sendSetupFormForChat(ctx, chatId, anchorMessageId, openId) {
148296
+ if (!ctx.threadRouting) {
148297
+ const title = defaultSessionTitle(ctx.now());
148298
+ await ctx.deps.createSession({ title, chatId, openId });
148299
+ await ctx.replyChat(chatId, `\u2705 \u5DF2\u65B0\u5EFA\u4F1A\u8BDD\u300C${title}\u300D`);
148300
+ return;
148301
+ }
148302
+ let state = await ctx.deps.wizard.get(chatId);
148303
+ if (!state) state = await ctx.deps.wizard.start(chatId, void 0, anchorMessageId);
148304
+ const card = await renderFormCard(ctx, state);
148305
+ const res = await ctx.deps.sender.sendCard(chatId, card);
148306
+ if (!res.ok) ctx.deps.log.warn("\u8868\u5355\u5361\u53D1\u9001\u5931\u8D25", { chatId, error: res.error ?? "unknown" });
147707
148307
  }
147708
- var RecentStore = class {
147709
- constructor(storage, log2, limits) {
147710
- this.storage = storage;
147711
- this.log = log2;
147712
- this.limits = limits;
148308
+ async function applySetupCardAction(ctx, action, value3) {
148309
+ const chatId = action.chatId;
148310
+ if (value3.kind === "confirm") {
148311
+ await confirmSetup(ctx, chatId, action);
148312
+ return;
147713
148313
  }
147714
- storage;
147715
- log;
147716
- limits;
147717
- async listDirs() {
147718
- const raw = await this.read(RECENT_DIRS_KEY);
147719
- return Array.isArray(raw) ? raw.filter((v) => typeof v === "string") : [];
148314
+ if (value3.kind === "cancel") {
148315
+ await ctx.deps.wizard.cancel(chatId);
148316
+ await ctx.patchCard(action.messageId, buildSetupDoneCard("\u2716\uFE0F \u5DF2\u53D6\u6D88", ["\u5EFA\u4F1A\u8BDD\u5411\u5BFC\u5DF2\u53D6\u6D88\u3002\u53D1\u9001 `/new` \u91CD\u65B0\u5F00\u59CB\u3002"]));
148317
+ return;
147720
148318
  }
147721
- async addDir(dir) {
147722
- const value3 = dir.trim();
147723
- if (!value3) return;
147724
- const list = [value3, ...(await this.listDirs()).filter((d) => d !== value3)];
147725
- await this.write(RECENT_DIRS_KEY, list.slice(0, Math.max(1, this.limits.dirs)));
148319
+ if (value3.kind === "form") {
148320
+ const state2 = await ctx.deps.wizard.get(chatId) ?? await ctx.deps.wizard.start(chatId, void 0, action.messageId);
148321
+ await ctx.patchCard(action.messageId, await renderFormCard(ctx, state2));
148322
+ return;
147726
148323
  }
147727
- async listModels() {
147728
- const raw = await this.read(RECENT_MODELS_KEY);
147729
- if (!Array.isArray(raw)) return [];
147730
- return raw.map(parseModel2).filter((m) => Boolean(m));
148324
+ if (value3.kind !== "dir" && value3.sid) {
148325
+ if (value3.kind === "model") {
148326
+ try {
148327
+ const outcome = await ctx.deps.switchSessionModel(value3.sid, value3.model);
148328
+ await ctx.deps.recent.addModel(value3.model);
148329
+ await ctx.patchCard(action.messageId, modelSwitchDoneCard(outcome, value3.model));
148330
+ } catch (err2) {
148331
+ ctx.deps.log.warn("\u5361\u7247\u5207\u6362\u6A21\u578B\u5931\u8D25", { sessionID: value3.sid, error: errorMessage(err2) });
148332
+ await ctx.patchCard(
148333
+ action.messageId,
148334
+ buildSetupDoneCard("\u26A0\uFE0F \u5207\u6362\u6A21\u578B\u5931\u8D25", [errorMessage(err2)])
148335
+ );
148336
+ }
148337
+ return;
148338
+ }
148339
+ if (value3.kind === "perm") {
148340
+ await ctx.deps.applyPermissionPreset(value3.sid, value3.preset);
148341
+ await ctx.patchCard(
148342
+ action.messageId,
148343
+ buildSetupDoneCard("\u2705 \u5DF2\u66F4\u65B0\u6743\u9650", [`\u5F53\u524D\u6743\u9650\uFF1A${presetLabel(value3.preset)}`])
148344
+ );
148345
+ return;
148346
+ }
148347
+ if (value3.kind === "more") {
148348
+ const current = await currentModel(ctx, value3.sid);
148349
+ const models = await ctx.loadModels();
148350
+ const recent = await ctx.deps.recent.listModels();
148351
+ const card = buildModelCard({
148352
+ models,
148353
+ recent,
148354
+ ...current ? { current } : {},
148355
+ page: value3.page,
148356
+ pageSize: ctx.modelPageSize,
148357
+ recentLimit: ctx.recentModelsLimit(),
148358
+ sid: value3.sid
148359
+ });
148360
+ await ctx.patchCard(action.messageId, card);
148361
+ return;
148362
+ }
148363
+ return;
147731
148364
  }
147732
- async addModel(model) {
147733
- if (!model.providerID || !model.id) return;
147734
- const key = modelKey(model);
147735
- const list = [model, ...(await this.listModels()).filter((m) => modelKey(m) !== key)];
147736
- await this.write(RECENT_MODELS_KEY, list.slice(0, Math.max(1, this.limits.models)));
148365
+ const state = await ctx.deps.wizard.get(chatId);
148366
+ if (!state) {
148367
+ await ctx.patchCard(action.messageId, buildSetupDoneCard("\u26A0\uFE0F \u5411\u5BFC\u5DF2\u5931\u6548", ["\u8BF7\u91CD\u65B0\u53D1\u9001 `/new` \u5F00\u59CB\u3002"]));
148368
+ return;
147737
148369
  }
147738
- async read(key) {
147739
- try {
147740
- return await this.storage.get(key);
147741
- } catch (err2) {
147742
- this.log.warn("\u6700\u8FD1\u8BB0\u5F55\u8BFB\u53D6\u5931\u8D25", { key, error: errorMessage(err2) });
147743
- return void 0;
147744
- }
148370
+ if (value3.kind === "dir") {
148371
+ const next = await ctx.deps.wizard.apply(chatId, { type: "setDir", dir: value3.dir });
148372
+ await ctx.deps.recent.addDir(value3.dir);
148373
+ if (!next) return;
148374
+ await ctx.patchCard(action.messageId, await renderModelCard(ctx, next));
148375
+ return;
147745
148376
  }
147746
- async write(key, value3) {
147747
- try {
147748
- await this.storage.set(key, value3);
147749
- } catch (err2) {
147750
- this.log.warn("\u6700\u8FD1\u8BB0\u5F55\u5199\u5165\u5931\u8D25", { key, error: errorMessage(err2) });
147751
- }
148377
+ if (value3.kind === "model") {
148378
+ const next = await ctx.deps.wizard.apply(chatId, { type: "setModel", model: value3.model });
148379
+ await ctx.deps.recent.addModel(value3.model);
148380
+ if (!next) return;
148381
+ await ctx.patchCard(action.messageId, buildPermCard({ ...next.perm ? { current: next.perm } : {} }));
148382
+ return;
148383
+ }
148384
+ if (value3.kind === "perm") {
148385
+ const next = await ctx.deps.wizard.apply(chatId, { type: "setPerm", perm: value3.preset });
148386
+ if (!next) return;
148387
+ await ctx.patchCard(action.messageId, buildConfirmCard(confirmInput(next)));
148388
+ return;
148389
+ }
148390
+ if (value3.kind === "more") {
148391
+ const next = await ctx.deps.wizard.apply(chatId, { type: "setPage", page: value3.page });
148392
+ if (!next) return;
148393
+ await ctx.patchCard(action.messageId, await renderModelCard(ctx, next, value3.page));
147752
148394
  }
147753
- };
147754
-
147755
- // src/feishu/topic-guidance.ts
147756
- var TOPIC_GUIDANCE_MARKER = "opencode-feishu-topic-guidance";
147757
- function buildTopicGuidance(title) {
147758
- return `\u672C\u4F1A\u8BDD\u4E3B\u9898\uFF1A\u300C${title}\u300D\u3002\u82E5\u7528\u6237\u660E\u663E\u8F6C\u5411\u4E0E\u8BE5\u4E3B\u9898\u65E0\u5173\u7684\u4EFB\u52A1\uFF0C\u53EF\u7B80\u77ED\u63D0\u9192\u4ED6\u7528 /new \u5F00\u65B0\u4F1A\u8BDD\uFF1B\u4E0D\u8981\u56E0\u6B64\u62D2\u7EDD\u56DE\u7B54\uFF0C\u4E5F\u4E0D\u8981\u957F\u7BC7\u8BF4\u6559\u3002`;
147759
148395
  }
147760
- async function injectTopicGuidance(input, deps) {
147761
- try {
147762
- if (!Array.isArray(input.system)) return;
147763
- const system = input.system;
147764
- if (system.some(isGuidancePart)) return;
147765
- const link2 = await deps.resolveSession(input.sessionID);
147766
- if (!link2) return;
147767
- const title = (await deps.resolveTitle(input.sessionID, link2.chatId))?.trim();
147768
- if (!title) return;
147769
- system.push({
147770
- type: "text",
147771
- text: buildTopicGuidance(title),
147772
- metadata: { [TOPIC_GUIDANCE_MARKER]: true }
148396
+ async function confirmSetup(ctx, chatId, action) {
148397
+ const state = await ctx.deps.wizard.get(chatId);
148398
+ if (!state || state.step !== "confirm" || !state.dir || !state.perm) {
148399
+ await ctx.patchCard(action.messageId, buildSetupDoneCard("\u26A0\uFE0F \u5411\u5BFC\u72B6\u6001\u4E0D\u5B8C\u6574", ["\u8BF7\u91CD\u65B0\u53D1\u9001 `/new` \u5F00\u59CB\u3002"]));
148400
+ return;
148401
+ }
148402
+ await ctx.deps.wizard.cancel(chatId);
148403
+ const title = state.title?.trim() || defaultSessionTitle(ctx.now());
148404
+ await createSessionFromSetup(ctx, chatId, action, {
148405
+ title,
148406
+ dir: state.dir,
148407
+ perm: state.perm,
148408
+ ...state.model ? { model: state.model } : {},
148409
+ ...state.anchorMessageId ? { anchorMessageId: state.anchorMessageId } : {}
148410
+ });
148411
+ }
148412
+ async function applySetupFormSubmit(ctx, action) {
148413
+ const fv = action.formValue;
148414
+ const fvKeys = fv && typeof fv === "object" ? Object.keys(fv) : [];
148415
+ ctx.deps.log.info("\u8868\u5355\u63D0\u4EA4\u8FDB\u5165\u5904\u7406", {
148416
+ chatId: action.chatId,
148417
+ messageId: action.messageId,
148418
+ formValueKeys: fvKeys,
148419
+ formValueType: Array.isArray(fv) ? "array" : typeof fv
148420
+ });
148421
+ const values2 = parseSetupFormValues(action.formValue);
148422
+ if (!values2) {
148423
+ ctx.deps.log.warn("\u8868\u5355\u6570\u636E\u7F3A\u5931\uFF08parse \u8FD4\u56DE undefined\uFF09", { formValueKeys: fvKeys });
148424
+ await ctx.patchCard(action.messageId, buildSetupDoneCard("\u26A0\uFE0F \u8868\u5355\u6570\u636E\u7F3A\u5931", ["\u8BF7\u91CD\u65B0\u53D1\u9001 `/form` \u586B\u5199\u3002"]));
148425
+ return;
148426
+ }
148427
+ const state = await ctx.deps.wizard.get(action.chatId);
148428
+ if (!state) {
148429
+ ctx.deps.log.warn("\u8868\u5355\u63D0\u4EA4\u4F46\u5411\u5BFC\u72B6\u6001\u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F/\u5DF2\u88AB\u6D88\u8D39\uFF09", {
148430
+ chatId: action.chatId,
148431
+ dirLen: values2.dir.length,
148432
+ hasModel: Boolean(values2.model),
148433
+ hasPerm: Boolean(values2.perm)
147773
148434
  });
148435
+ await ctx.patchCard(action.messageId, buildSetupDoneCard("\u26A0\uFE0F \u8868\u5355\u5DF2\u5931\u6548", ["\u8BF7\u91CD\u65B0\u53D1\u9001 `/form` \u6216 `/new` \u6253\u5F00\u8868\u5355\u3002"]));
148436
+ return;
148437
+ }
148438
+ const title = state.title?.trim() || defaultSessionTitle(ctx.now());
148439
+ const requestedDir = resolveSetupFormDir(values2, ctx.deps.allowedRoots ?? []);
148440
+ let preserved = {
148441
+ dir: requestedDir,
148442
+ ...values2.model ? { model: values2.model } : {},
148443
+ ...values2.perm ? { perm: values2.perm } : {}
148444
+ };
148445
+ ctx.deps.log.info("\u8868\u5355\u5B57\u6BB5\u89E3\u6790", {
148446
+ dirInput: values2.dir,
148447
+ dirSelect: values2.dirSelect,
148448
+ dir: requestedDir,
148449
+ model: values2.model ? `${values2.model.providerID}/${values2.model.id}` : void 0,
148450
+ perm: values2.perm,
148451
+ hasState: true
148452
+ });
148453
+ const validation = ctx.deps.validateDir(requestedDir);
148454
+ if (!validation.ok) {
148455
+ ctx.deps.log.warn("\u76EE\u5F55\u6821\u9A8C\u5931\u8D25", { dir: requestedDir, reason: validation.message });
148456
+ await ctx.patchCard(
148457
+ action.messageId,
148458
+ await renderFormCard(ctx, state, { error: validation.message, values: preserved })
148459
+ );
148460
+ return;
148461
+ }
148462
+ preserved = { ...preserved, dir: validation.path };
148463
+ if (!values2.perm) {
148464
+ ctx.deps.log.warn("\u6743\u9650\u6863\u4F4D\u7F3A\u5931", { dir: validation.path });
148465
+ await ctx.patchCard(
148466
+ action.messageId,
148467
+ await renderFormCard(ctx, state, { error: "\u8BF7\u9009\u62E9\u6743\u9650\u6863\u4F4D\u3002", values: { ...preserved, dir: validation.path } })
148468
+ );
148469
+ return;
148470
+ }
148471
+ const model = values2.model ? await resolveFormModel(ctx, values2.model) : state?.model;
148472
+ ctx.deps.log.info("\u8868\u5355\u6821\u9A8C\u901A\u8FC7\uFF0C\u5F00\u59CB\u5EFA\u4F1A\u8BDD", {
148473
+ dir: validation.path,
148474
+ perm: values2.perm,
148475
+ model: model ? `${model.providerID}/${model.id}` : void 0
148476
+ });
148477
+ await ctx.deps.wizard.cancel(action.chatId);
148478
+ await createSessionFromSetup(ctx, action.chatId, action, {
148479
+ title,
148480
+ dir: validation.path,
148481
+ perm: values2.perm,
148482
+ ...model ? { model } : {}
148483
+ });
148484
+ }
148485
+ async function resolveFormModel(ctx, ref) {
148486
+ const models = await ctx.loadModels();
148487
+ const matched = matchModel(`${ref.providerID}/${ref.id}`, models);
148488
+ return matched.ok ? matched.model : ref;
148489
+ }
148490
+ async function createSessionFromSetup(ctx, chatId, action, opts) {
148491
+ const { title, dir, perm, model } = opts;
148492
+ const permissions = presetToRuleset(perm);
148493
+ const gateMode = presetGateMode(perm);
148494
+ const created = await ctx.deps.createSession({
148495
+ title,
148496
+ chatId,
148497
+ openId: action.operatorOpenId,
148498
+ setActive: true,
148499
+ directory: dir,
148500
+ permissions,
148501
+ perm,
148502
+ gateMode,
148503
+ ...model ? { model } : {}
148504
+ });
148505
+ await ctx.deps.recent.addDir(dir);
148506
+ if (model) await ctx.deps.recent.addModel(model);
148507
+ const readyCard = buildSessionReadyCard({
148508
+ title,
148509
+ sessionID: created.id,
148510
+ dir,
148511
+ ...model ? { model: modelLabel(model) } : {},
148512
+ perm: presetLabel(perm)
148513
+ });
148514
+ const anchorId = action.messageId || opts.anchorMessageId || "";
148515
+ const res = await ctx.deps.sender.replyCard(anchorId, readyCard, { replyInThread: true });
148516
+ ctx.deps.log.info("\u521B\u5EFA\u4F1A\u8BDD\u5E76\u5F00\u8BDD\u9898", {
148517
+ sessionID: created.id,
148518
+ anchorMessageId: anchorId,
148519
+ anchorFromFormCard: Boolean(action.messageId),
148520
+ replyOk: res.ok,
148521
+ replyMessageId: res.messageId,
148522
+ replyThreadId: res.threadId,
148523
+ replyError: res.error
148524
+ });
148525
+ if (!res.ok || !res.messageId) {
148526
+ ctx.deps.log.warn("\u4E00\u952E\u5F00\u8BDD\u9898\u5931\u8D25", { error: res.error ?? "unknown" });
148527
+ await ctx.patchCard(
148528
+ action.messageId,
148529
+ buildSessionCreatedCard({
148530
+ title,
148531
+ sessionID: created.id,
148532
+ dir,
148533
+ ...model ? { model: modelLabel(model) } : {},
148534
+ perm: presetLabel(perm),
148535
+ note: "\u26A0\uFE0F \u81EA\u52A8\u5F00\u8BDD\u9898\u5931\u8D25\uFF1A\u8BF7\u5728 `/sessions` \u7684\u4F1A\u8BDD\u5361\u4E0A\u624B\u52A8\u300C\u521B\u5EFA\u8BDD\u9898\u300D\uFF0C\u6216\u5728\u4E3B\u804A\u5929\u6D41\u7528 `/use` \u5207\u6362\u540E\u7EE7\u7EED\u3002"
148536
+ })
148537
+ );
148538
+ return;
148539
+ }
148540
+ await ctx.deps.sessionMap.bindRoot(anchorId, created.id);
148541
+ const meta = res.threadId ? void 0 : await ctx.deps.sender.getMessageMeta(res.messageId);
148542
+ const threadId = res.threadId ?? meta?.threadId;
148543
+ if (threadId) {
148544
+ await ctx.deps.sessionMap.bindThread(threadId, created.id, chatId, action.operatorOpenId, anchorId);
148545
+ } else {
148546
+ ctx.deps.log.warn("\u4E00\u952E\u5F00\u8BDD\u9898\u540E\u672A\u8BFB\u5230 thread_id\uFF0C\u8BE5\u4F1A\u8BDD\u6682\u65E0\u6CD5\u81EA\u52A8\u8DEF\u7531", { messageId: res.messageId });
148547
+ }
148548
+ await ctx.patchCard(
148549
+ action.messageId,
148550
+ buildSessionCreatedCard({
148551
+ title,
148552
+ sessionID: created.id,
148553
+ dir,
148554
+ ...model ? { model: modelLabel(model) } : {},
148555
+ perm: presetLabel(perm),
148556
+ ...threadId ? {} : { note: "\uFF08\u672A\u62FF\u5230\u8BDD\u9898 ID\uFF0C\u82E5\u8BDD\u9898\u672A\u51FA\u73B0\u8BF7\u5728\u4F1A\u8BDD\u5361\u4E0A\u624B\u52A8\u521B\u5EFA\u3002\uFF09" }
148557
+ })
148558
+ );
148559
+ }
148560
+ async function renderFormCard(ctx, state, over) {
148561
+ const models = await ctx.loadModels();
148562
+ const recent = await ctx.deps.recent.listModels();
148563
+ const rootSubdirs = await scanRoot(ctx, ctx.deps.allowedRoots?.[0]);
148564
+ const values2 = over?.values ?? {
148565
+ ...state?.dir ? { dir: state.dir } : {},
148566
+ ...state?.perm ? { perm: state.perm } : {}
148567
+ };
148568
+ return buildSetupFormCard({
148569
+ models,
148570
+ recent,
148571
+ ...state?.model ? { defaultModel: state.model } : {},
148572
+ ...rootSubdirs.length > 0 ? { rootSubdirs } : {},
148573
+ ...ctx.deps.allowedRoots ? { allowedRoots: ctx.deps.allowedRoots } : {},
148574
+ ...over?.error ? { error: over.error } : {},
148575
+ values: values2
148576
+ });
148577
+ }
148578
+ async function scanRoot(ctx, root) {
148579
+ if (!root) return [];
148580
+ const scan = ctx.deps.scanRootSubdirs ?? scanRootSubdirs;
148581
+ try {
148582
+ return [...await scan(root)];
147774
148583
  } catch (err2) {
147775
- deps.log.warn("\u4E3B\u9898\u8F6F\u5F15\u5BFC\u6CE8\u5165\u5931\u8D25", { sessionID: input.sessionID, error: errorMessage(err2) });
148584
+ ctx.deps.log.warn("\u6839\u76EE\u5F55\u5B50\u76EE\u5F55\u626B\u63CF\u5931\u8D25", { error: errorMessage(err2) });
148585
+ return [];
147776
148586
  }
147777
148587
  }
147778
- function isGuidancePart(part) {
147779
- if (typeof part !== "object" || part === null) return false;
147780
- const metadata = part.metadata;
147781
- if (typeof metadata !== "object" || metadata === null) return false;
147782
- return metadata[TOPIC_GUIDANCE_MARKER] === true;
148588
+ function confirmInput(state) {
148589
+ return {
148590
+ ...state.title ? { title: state.title } : {},
148591
+ ...state.dir ? { dir: state.dir } : {},
148592
+ ...state.model ? { model: state.model } : {},
148593
+ ...state.perm ? { perm: state.perm } : {}
148594
+ };
148595
+ }
148596
+ function createSetupWizardApi(ctx) {
148597
+ return {
148598
+ cmdNew: (message, args2) => cmdNew(ctx, message, args2),
148599
+ cmdForm: (message, args2) => cmdForm(ctx, message, args2),
148600
+ cmdDir: (message, args2) => cmdDir(ctx, message, args2),
148601
+ cmdModel: (message, args2, scope3) => cmdModel(ctx, message, args2, scope3),
148602
+ cmdPerm: (message, args2, scope3) => cmdPerm(ctx, message, args2, scope3),
148603
+ cmdCancel: (message) => cmdCancel(ctx, message),
148604
+ applySetupCardAction: (action, value3) => applySetupCardAction(ctx, action, value3),
148605
+ applySetupFormSubmit: (action) => applySetupFormSubmit(ctx, action),
148606
+ sendSetupFormForChat: (chatId, anchorMessageId, openId) => sendSetupFormForChat(ctx, chatId, anchorMessageId, openId)
148607
+ };
147783
148608
  }
147784
148609
 
147785
- // src/permission.ts
147786
- function decideEffect(action, config) {
147787
- if (config.permissionGate === "off" || config.permissionGate === "notify") {
147788
- return {};
148610
+ // src/session/session-list.ts
148611
+ async function cmdSessions(ctx, message, page = 0) {
148612
+ const entries3 = await loadSessionEntries(ctx, message.chatId);
148613
+ const active = await ctx.deps.sessionMap.getActive(message.chatId);
148614
+ const card = await buildListCard(ctx, message.chatId, entries3, page, active?.sessionID);
148615
+ const res = await ctx.deps.sender.sendCard(message.chatId, card);
148616
+ if (!res.ok) {
148617
+ ctx.deps.log.warn("\u4F1A\u8BDD\u5361\u7247\u53D1\u9001\u5931\u8D25", { chatId: message.chatId, error: res.error ?? "unknown" });
148618
+ }
148619
+ }
148620
+ async function loadSessionEntries(ctx, chatId) {
148621
+ if (ctx.deps.listAllSessions) {
148622
+ try {
148623
+ const raw = await ctx.deps.listAllSessions();
148624
+ const normalized = normalizeSessionList(raw);
148625
+ if (normalized) {
148626
+ ctx.deps.log.info("\u5217\u51FA\u5168\u90E8\u4F1A\u8BDD", { count: normalized.length, source: "session.list" });
148627
+ return normalized;
148628
+ }
148629
+ ctx.deps.log.warn("session.list \u8FD4\u56DE\u5F62\u72B6\u65E0\u6CD5\u8BC6\u522B\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", { chatId });
148630
+ } catch (err2) {
148631
+ ctx.deps.log.warn("session.list \u8C03\u7528\u5931\u8D25\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", {
148632
+ chatId,
148633
+ error: errorMessage(err2)
148634
+ });
148635
+ }
148636
+ } else {
148637
+ ctx.deps.log.warn("\u8FD0\u884C\u65F6\u672A\u66B4\u9732 session.list\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", { chatId });
147789
148638
  }
147790
- if (matchesAny(action, config.denyTools)) {
147791
- return { effect: "deny", message: `feishu policy: ${action} \u5DF2\u5728 denyTools` };
148639
+ const fallback = fallbackEntries(await ctx.deps.sessionMap.listSessions(chatId));
148640
+ ctx.deps.log.warn("\u4F1A\u8BDD\u5217\u8868\u56DE\u9000\u5230 SessionMap", { chatId, count: fallback.length });
148641
+ return fallback;
148642
+ }
148643
+ async function buildListCard(ctx, chatId, entries3, page, activeID) {
148644
+ const pageSize = ctx.sessionPageSize;
148645
+ const pageCount = Math.max(1, Math.ceil(entries3.length / pageSize));
148646
+ const safePage = Math.min(Math.max(0, Math.floor(page)), pageCount - 1);
148647
+ const slice = entries3.slice(safePage * pageSize, safePage * pageSize + pageSize);
148648
+ const rows = [];
148649
+ for (let i = 0; i < slice.length; i += 1) {
148650
+ const entry = slice[i];
148651
+ const bound = Boolean(await ctx.deps.sessionMap.threadIdForSession(entry.sessionID));
148652
+ rows.push({
148653
+ index: safePage * pageSize + i + 1,
148654
+ sessionID: entry.sessionID,
148655
+ title: entry.title,
148656
+ updatedAt: entry.updatedAt,
148657
+ ...entry.directory ? { directory: entry.directory } : {},
148658
+ bound,
148659
+ ...entry.sessionID === activeID ? { active: true } : {}
148660
+ });
147792
148661
  }
147793
- if (matchesAny(action, config.allowTools)) {
147794
- return { effect: "allow" };
148662
+ return buildSessionListCard({
148663
+ chatId,
148664
+ rows,
148665
+ page: safePage,
148666
+ pageCount,
148667
+ total: entries3.length,
148668
+ now: ctx.now()
148669
+ });
148670
+ }
148671
+ async function enterSessionThread(ctx, input) {
148672
+ const { chatId, sessionID, anchorMessageId, operatorOpenId } = input;
148673
+ if (!anchorMessageId || !chatId) {
148674
+ return { ok: false, error: "missing anchor/chat" };
147795
148675
  }
147796
- if (config.permissionGate === "lockdown") {
147797
- return { effect: "deny", message: `feishu lockdown: ${action} \u4E0D\u5728 allowTools` };
148676
+ const info = input.info;
148677
+ const card = buildSessionOpenedCard({
148678
+ title: info?.title ?? "",
148679
+ sessionID,
148680
+ ...info?.directory ? { dir: info.directory } : {},
148681
+ ...info?.updatedAt ? { updatedAt: info.updatedAt } : {},
148682
+ now: ctx.now()
148683
+ });
148684
+ const res = await ctx.deps.sender.replyCard(anchorMessageId, card, { replyInThread: true });
148685
+ ctx.deps.log.info("\u8FDB\u5165\u4F1A\u8BDD\u5E76\u5F00\u8BDD\u9898", {
148686
+ sessionID,
148687
+ source: input.source,
148688
+ anchorMessageId,
148689
+ replyOk: res.ok,
148690
+ replyMessageId: res.messageId,
148691
+ replyThreadId: res.threadId,
148692
+ replyError: res.error
148693
+ });
148694
+ if (!res.ok || !res.messageId) {
148695
+ const error = res.error ?? "unknown";
148696
+ ctx.deps.log.warn("\u8FDB\u5165\u8BDD\u9898\u5931\u8D25", { sessionID, anchorMessageId, error });
148697
+ if (input.patchMessageId) {
148698
+ await ctx.deps.sender.patchCard(
148699
+ input.patchMessageId,
148700
+ buildSessionMissingCard(sessionID, error)
148701
+ );
148702
+ }
148703
+ return { ok: false, error };
148704
+ }
148705
+ const meta = res.threadId ? void 0 : await ctx.deps.sender.getMessageMeta(res.messageId);
148706
+ const threadId = res.threadId ?? meta?.threadId;
148707
+ if (!threadId) {
148708
+ ctx.deps.log.warn("\u8FDB\u5165\u8BDD\u9898\u540E\u672A\u8BFB\u5230 thread_id\uFF0C\u8BE5\u4F1A\u8BDD\u6682\u65E0\u6CD5\u81EA\u52A8\u8DEF\u7531", { messageId: res.messageId });
148709
+ return { ok: false, error: "\u672A\u62FF\u5230\u8BDD\u9898 ID" };
148710
+ }
148711
+ await ctx.deps.sessionMap.bindThread(threadId, sessionID, chatId, operatorOpenId, anchorMessageId);
148712
+ await ctx.deps.sessionMap.bindRoot(res.messageId, sessionID);
148713
+ ctx.deps.log.info("\u5DF2\u7ED1\u5B9A\u8BDD\u9898\u4E0E\u4F1A\u8BDD", { sessionID, threadId, rootId: res.messageId });
148714
+ return { ok: true, threadId };
148715
+ }
148716
+ async function handleOpenCardAction(ctx, action, value3) {
148717
+ if (!ctx.threadRouting) {
148718
+ return toast3("error", "\u5F53\u524D\u4E3A\u56DE\u9000\u6A21\u5F0F\uFF0C\u4E0D\u652F\u6301\u8BDD\u9898\u8DEF\u7531");
148719
+ }
148720
+ const chatId = action.chatId || value3.chatId;
148721
+ const hasApi = Boolean(ctx.deps.getSessionInfo);
148722
+ const info = hasApi ? normalizeSessionInfo(await ctx.deps.getSessionInfo(value3.sessionID)) : void 0;
148723
+ if (hasApi && !info) {
148724
+ ctx.deps.log.warn("\u8FDB\u5165\u8BDD\u9898\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E0D\u5B58\u5728", { sessionID: value3.sessionID, chatId });
148725
+ void patchMissingCard(ctx, action.messageId, value3.sessionID).catch((err2) => {
148726
+ ctx.deps.log.warn("\u4F1A\u8BDD\u4E0D\u5B58\u5728\u63D0\u793A\u5361 patch \u5931\u8D25", { error: errorMessage(err2) });
148727
+ });
148728
+ return toast3("error", "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
147798
148729
  }
147799
- return { effect: "ask", message: `opencode-feishu-v2: \u9700\u8981\u4EBA\u5DE5\u6279\u51C6 ${action}` };
148730
+ const snapshot2 = info ?? { sessionID: value3.sessionID, title: "", updatedAt: 0 };
148731
+ void enterSessionThread(ctx, {
148732
+ chatId,
148733
+ sessionID: value3.sessionID,
148734
+ anchorMessageId: action.messageId,
148735
+ operatorOpenId: action.operatorOpenId,
148736
+ info: snapshot2,
148737
+ patchMessageId: action.messageId,
148738
+ source: "card"
148739
+ }).catch((err2) => {
148740
+ ctx.deps.log.warn("\u8FDB\u5165\u8BDD\u9898\u5904\u7406\u5931\u8D25", { sessionID: value3.sessionID, error: errorMessage(err2) });
148741
+ });
148742
+ return toast3("success", "\u6B63\u5728\u8FDB\u5165\u8BDD\u9898\u2026");
147800
148743
  }
147801
- function decideEffectForSession(action, config, session) {
147802
- if (!session) return decideEffect(action, config);
147803
- if (session.gateMode === "off") return {};
147804
- if (matchesAny(action, config.denyTools)) {
147805
- return { effect: "deny", message: `feishu policy: ${action} \u5DF2\u5728 denyTools` };
148744
+ async function patchMissingCard(ctx, messageId, sessionID) {
148745
+ if (!messageId) return;
148746
+ await ctx.deps.sender.patchCard(messageId, buildSessionMissingCard(sessionID));
148747
+ }
148748
+ async function applySessionCardAction(ctx, action, value3) {
148749
+ const chatId = action.chatId || value3.chatId;
148750
+ if (!chatId) return;
148751
+ if (value3.cmd === "new") {
148752
+ await sendSetupFormForChat(ctx, chatId, action.messageId, action.operatorOpenId);
148753
+ return;
147806
148754
  }
147807
- if (matchesAny(action, config.allowTools)) {
147808
- return { effect: "allow" };
148755
+ if (value3.cmd === "list") {
148756
+ if (action.messageId) await patchListCard(ctx, chatId, action.messageId, value3.page);
148757
+ return;
147809
148758
  }
147810
- if (session.askActions && matchesAny(action, session.askActions)) {
147811
- return { effect: "ask", message: `opencode-feishu-v2: \u4F1A\u8BDD\u9884\u8BBE\u9700\u4EBA\u5DE5\u6279\u51C6 ${action}` };
148759
+ if (value3.cmd === "open") {
148760
+ return;
147812
148761
  }
147813
- return {};
148762
+ const ok = await ctx.deps.sessionMap.setActive(chatId, value3.sessionID);
148763
+ if (!ok) ctx.deps.log.debug("\u5361\u7247\u5207\u6362\u4F1A\u8BDD\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E0D\u5B58\u5728", { sessionID: value3.sessionID });
148764
+ if (action.messageId) await patchListCard(ctx, chatId, action.messageId);
147814
148765
  }
147815
- function parseApprovalValue(rawValue) {
147816
- if (typeof rawValue !== "object" || rawValue === null) return void 0;
147817
- const record2 = rawValue;
147818
- const token = typeof record2.t === "string" ? record2.t : "";
147819
- const d = record2.d;
147820
- if (!token) return void 0;
147821
- if (d !== "once" && d !== "always" && d !== "reject") return void 0;
147822
- return { token, decision: d };
148766
+ async function patchListCard(ctx, chatId, messageId, page = 0) {
148767
+ const entries3 = await loadSessionEntries(ctx, chatId);
148768
+ const active = await ctx.deps.sessionMap.getActive(chatId);
148769
+ const card = await buildListCard(ctx, chatId, entries3, page, active?.sessionID);
148770
+ const res = await ctx.deps.sender.patchCard(messageId, card);
148771
+ if (!res.ok) ctx.deps.log.warn("\u4F1A\u8BDD\u5361\u7247\u66F4\u65B0\u5931\u8D25", { error: res.error ?? "unknown" });
147823
148772
  }
147824
- var ApprovalManager = class {
147825
- constructor(deps) {
147826
- this.deps = deps;
147827
- this.now = deps.now ?? (() => Date.now());
148773
+ function createSessionListApi(ctx) {
148774
+ return {
148775
+ cmdSessions: (message, page) => cmdSessions(ctx, message, page),
148776
+ loadSessionEntries: (chatId) => loadSessionEntries(ctx, chatId),
148777
+ buildListCard: (chatId, entries3, page, activeID) => buildListCard(ctx, chatId, entries3, page, activeID),
148778
+ enterSessionThread: (input) => enterSessionThread(ctx, input),
148779
+ handleOpenCardAction: (action, value3) => handleOpenCardAction(ctx, action, value3),
148780
+ patchMissingCard: (messageId, sessionID) => patchMissingCard(ctx, messageId, sessionID),
148781
+ applySessionCardAction: (action, value3) => applySessionCardAction(ctx, action, value3),
148782
+ patchListCard: (chatId, messageId, page) => patchListCard(ctx, chatId, messageId, page)
148783
+ };
148784
+ }
148785
+
148786
+ // src/session/session-ops.ts
148787
+ async function cmdUse(ctx, message, args2) {
148788
+ const sessions = await ctx.deps.sessionMap.listSessions(message.chatId);
148789
+ if (sessions.length === 0) {
148790
+ await ctx.reply(message, "\u8FD8\u6CA1\u6709\u4F1A\u8BDD\u3002\u4F7F\u7528 /new \u65B0\u5EFA\u4E00\u4E2A\u3002");
148791
+ return;
147828
148792
  }
147829
- deps;
147830
- seenRequests = new TtlMap(60 * 60 * 1e3);
147831
- cards = new TtlMap(60 * 60 * 1e3);
147832
- now;
147833
- /** 处理 permission.asked:发审批卡。 */
147834
- async onAsked(request3) {
147835
- const requestID = request3.id;
147836
- if (!requestID || !this.seenRequests.setIfAbsent(requestID, true)) return;
147837
- const link2 = await this.deps.getLink(request3.sessionID);
147838
- if (!link2) {
147839
- this.deps.log.debug("permission.asked \u65E0\u98DE\u4E66\u6620\u5C04\uFF0C\u8DF3\u8FC7\u5BA1\u6279\u5361", {
147840
- requestID,
147841
- sessionID: request3.sessionID
147842
- });
148793
+ const matched = matchSession(args2, sessions);
148794
+ if (!matched.ok) {
148795
+ await ctx.reply(message, useErrorText(matched.reason));
148796
+ return;
148797
+ }
148798
+ const entry = matched.entry;
148799
+ const ok = await ctx.deps.sessionMap.setActive(message.chatId, entry.sessionID);
148800
+ if (!ok) {
148801
+ await ctx.reply(message, "\u5207\u6362\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u5148\u7528 /sessions \u67E5\u770B\u5217\u8868\u3002");
148802
+ return;
148803
+ }
148804
+ await ctx.reply(message, `\u2705 \u5DF2\u5207\u6362\u5230\u300C${entry.title.trim() || "(\u672A\u547D\u540D)"}\u300D
148805
+ \`${entry.sessionID}\``);
148806
+ }
148807
+ async function cmdCurrent(ctx, message, scope3) {
148808
+ if (scope3 === "thread") {
148809
+ const sessionID = await ctx.threadSessionID(message);
148810
+ if (!sessionID) {
148811
+ await ctx.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\u3002\u8BF7\u56DE\u5230\u4E3B\u804A\u5929\u6D41\u7528 `/new` \u65B0\u5EFA\u3002");
147843
148812
  return;
147844
148813
  }
147845
- const canPersistAlways = Array.isArray(request3.save) && request3.save.length > 0;
147846
- const token = this.deps.sign({
147847
- requestID,
147848
- sessionID: request3.sessionID,
147849
- openId: link2.openId
147850
- });
147851
- const input = {
147852
- requestID,
147853
- sessionID: request3.sessionID,
147854
- action: request3.action,
147855
- resources: request3.resources ?? [],
147856
- ...request3.message ? { message: request3.message } : {},
147857
- canPersistAlways,
147858
- token,
147859
- maxResourcesShown: this.deps.config.maxResourcesShown
147860
- };
147861
- const result3 = link2.replyMessageId ? await this.deps.sender.replyCard(link2.replyMessageId, buildApprovalCard(input)) : await this.deps.sender.sendCard(link2.chatId, buildApprovalCard(input));
147862
- if (!result3.ok || !result3.messageId) {
147863
- this.deps.log.warn("\u5BA1\u6279\u5361\u53D1\u9001\u5931\u8D25", { requestID, error: result3.error ?? "unknown" });
148814
+ const entry = await ctx.deps.sessionMap.getSession(message.chatId, sessionID);
148815
+ const link2 = await ctx.deps.sessionMap.resolveBySession(sessionID);
148816
+ const model = await currentModel(ctx, sessionID) ?? link2?.model;
148817
+ const lines = [
148818
+ `\u672C\u8BDD\u9898\u4F1A\u8BDD\uFF1A\u300C${entry?.title.trim() || "(\u672A\u547D\u540D)"}\u300D`,
148819
+ `\`${sessionID}\``,
148820
+ ...link2?.dir ? [`\u76EE\u5F55\uFF1A\`${link2.dir}\``] : [],
148821
+ ...model ? [`\u6A21\u578B\uFF1A${modelLabel(model)}`] : [],
148822
+ ...link2?.perm ? [`\u6743\u9650\uFF1A${presetLabel(link2.perm)}`] : []
148823
+ ];
148824
+ await ctx.reply(message, lines.join("\n"));
148825
+ return;
148826
+ }
148827
+ const active = await ctx.deps.sessionMap.getActive(message.chatId);
148828
+ if (!active) {
148829
+ await ctx.reply(message, "\u5F53\u524D\u6CA1\u6709\u4F1A\u8BDD\u3002\u4F7F\u7528 /new \u65B0\u5EFA\u4E00\u4E2A\u3002");
148830
+ return;
148831
+ }
148832
+ const count = (await ctx.deps.sessionMap.listSessions(message.chatId)).length;
148833
+ await ctx.reply(
148834
+ message,
148835
+ `\u5F53\u524D\u4F1A\u8BDD\uFF1A\u300C${active.title.trim() || "(\u672A\u547D\u540D)"}\u300D
148836
+ \`${active.sessionID}\`
148837
+ \u5171 ${count} \u4E2A\u4F1A\u8BDD\u3002`
148838
+ );
148839
+ }
148840
+ async function cmdStop(ctx, message, scope3) {
148841
+ if (scope3 === "thread") {
148842
+ const sessionID = await ctx.threadSessionID(message);
148843
+ if (!sessionID) {
148844
+ await ctx.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\uFF0C\u65E0\u6CD5\u4E2D\u65AD\u3002");
147864
148845
  return;
147865
148846
  }
147866
- this.cards.set(requestID, {
147867
- messageId: result3.messageId,
147868
- input,
147869
- ...link2.dir ? { directory: link2.dir } : {},
147870
- resolved: false
147871
- });
147872
- this.deps.log.info("\u5BA1\u6279\u5361\u5DF2\u53D1\u9001", {
147873
- requestID,
147874
- action: request3.action,
147875
- canPersistAlways,
147876
- hasDir: Boolean(link2.dir)
147877
- });
148847
+ await ctx.deps.interruptSession(sessionID);
148848
+ const entry = await ctx.deps.sessionMap.getSession(message.chatId, sessionID);
148849
+ await ctx.reply(message, `\u23F9\uFE0F \u5DF2\u8BF7\u6C42\u4E2D\u65AD\u672C\u8BDD\u9898\u4F1A\u8BDD\uFF1A\u300C${entry?.title.trim() || "(\u672A\u547D\u540D)"}\u300D`);
148850
+ return;
147878
148851
  }
147879
- /** 处理 permission.replied:把卡片收敛为结果态(若尚未由点击更新)。 */
147880
- onReplied(event) {
147881
- const tracked = this.cards.get(event.requestID);
147882
- if (!tracked) return;
147883
- if (!tracked.resolved) {
147884
- const outcome = {
147885
- reply: event.reply,
147886
- operatorOpenId: "",
147887
- at: this.now()
147888
- };
147889
- void this.patchResolved(tracked, outcome, event.requestID);
147890
- }
147891
- this.cards.delete(event.requestID);
148852
+ const active = await ctx.deps.sessionMap.getActive(message.chatId);
148853
+ if (!active) {
148854
+ await ctx.reply(message, "\u5F53\u524D\u6CA1\u6709\u4F1A\u8BDD\u53EF\u4E2D\u65AD\u3002");
148855
+ return;
147892
148856
  }
147893
- /**
147894
- * 处理卡片按钮点击。同步返回飞书回调响应(toast),reply 在后台完成。
147895
- * 校验顺序:白名单 → 签名 → 绑定字段 → 防重放。
147896
- */
147897
- handleCardAction(action) {
147898
- const parsed = parseApprovalValue(action.rawValue);
147899
- if (!parsed) {
147900
- return toast2("error", "\u65E0\u6CD5\u8BC6\u522B\u7684\u64CD\u4F5C");
147901
- }
147902
- if (!this.deps.isAllowed(action.operatorOpenId)) {
147903
- this.deps.log.warn("\u62D2\u7EDD\u975E\u767D\u540D\u5355\u7528\u6237\u7684\u5BA1\u6279\u70B9\u51FB", {
147904
- operator: action.operatorOpenId.slice(0, 8)
147905
- });
147906
- return toast2("error", "\u65E0\u5BA1\u6279\u6743\u9650");
147907
- }
147908
- const verified = this.deps.verify(parsed.token, { u: action.operatorOpenId });
147909
- if (!verified.ok) {
147910
- this.deps.log.warn("\u5BA1\u6279 token \u6821\u9A8C\u5931\u8D25", { reason: verified.reason });
147911
- return toast2("error", `\u5BA1\u6279\u51ED\u8BC1\u65E0\u6548\uFF08${verified.reason}\uFF09`);
147912
- }
147913
- const claims = verified.claims;
147914
- const ttl = Math.max(1e3, claims.e - this.now());
147915
- if (!this.deps.replay.consume(claims.n, ttl)) {
147916
- return toast2("warning", "\u8BE5\u64CD\u4F5C\u5DF2\u5904\u7406\uFF0C\u8BF7\u52FF\u91CD\u590D\u70B9\u51FB");
147917
- }
147918
- const tracked = this.cards.get(claims.r);
147919
- const outcome = {
147920
- reply: parsed.decision,
147921
- operatorOpenId: action.operatorOpenId,
147922
- at: this.now()
147923
- };
147924
- void this.applyReply(claims.r, claims.s, parsed.decision, tracked, outcome, tracked?.directory);
147925
- return toast2(
147926
- parsed.decision === "reject" ? "warning" : "success",
147927
- parsed.decision === "reject" ? "\u5DF2\u62D2\u7EDD" : "\u5DF2\u5141\u8BB8"
147928
- );
148857
+ await ctx.deps.interruptSession(active.sessionID);
148858
+ await ctx.reply(message, `\u23F9\uFE0F \u5DF2\u8BF7\u6C42\u4E2D\u65AD\u5F53\u524D\u4F1A\u8BDD\uFF1A\u300C${active.title.trim() || "(\u672A\u547D\u540D)"}\u300D`);
148859
+ }
148860
+ async function cmdCd(ctx, message, args2) {
148861
+ if (!message.threadId) {
148862
+ await ctx.reply(message, "`/cd` \u53EA\u80FD\u5728\u8BDD\u9898\u5185\u4F7F\u7528\uFF08\u7528\u4E8E\u79FB\u52A8\u8BE5\u8BDD\u9898\u4F1A\u8BDD\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF09\u3002");
148863
+ return;
147929
148864
  }
147930
- dispose() {
147931
- this.seenRequests.clear();
147932
- this.cards.clear();
148865
+ const sessionID = await ctx.threadSessionID(message);
148866
+ if (!sessionID) {
148867
+ await ctx.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\u3002\u8BF7\u56DE\u5230\u4E3B\u804A\u5929\u6D41\u7528 `/new` \u65B0\u5EFA\u3002");
148868
+ return;
147933
148869
  }
147934
- async applyReply(requestID, sessionID, reply, tracked, outcome, directory) {
147935
- try {
147936
- await this.deps.reply({ sessionID, requestID, reply, ...directory ? { directory } : {} });
147937
- } catch (err2) {
147938
- this.deps.log.error("permission.reply \u5931\u8D25", {
147939
- requestID,
147940
- hasDir: Boolean(directory),
147941
- error: errorMessage(err2)
147942
- });
148870
+ const validation = ctx.deps.validateDir(args2);
148871
+ if (!validation.ok) {
148872
+ await ctx.reply(message, validation.message);
148873
+ return;
148874
+ }
148875
+ await ctx.deps.moveSessionDir(sessionID, validation.path);
148876
+ await ctx.deps.recent.addDir(validation.path);
148877
+ await ctx.reply(message, `\u2705 \u5DF2\u5207\u6362\u672C\u4F1A\u8BDD\u76EE\u5F55\uFF1A\`${validation.path}\``);
148878
+ }
148879
+ async function cmdResume(ctx, message, args2) {
148880
+ if (message.threadId) {
148881
+ await ctx.reply(message, threadForbiddenText("resume"));
148882
+ return;
148883
+ }
148884
+ if (!ctx.threadRouting) {
148885
+ await ctx.reply(message, "\u5F53\u524D\u4E3A\u56DE\u9000\u6A21\u5F0F\uFF08`threadRouting=false`\uFF09\uFF0C\u4E0D\u652F\u6301\u7EED\u804A\u5386\u53F2\u4F1A\u8BDD\u3002");
148886
+ return;
148887
+ }
148888
+ const entries3 = await loadSessionEntries(ctx, message.chatId);
148889
+ if (entries3.length === 0) {
148890
+ await ctx.reply(message, "\u6CA1\u6709\u53EF\u7EED\u804A\u7684\u4F1A\u8BDD\u3002\u7528 `/new` \u65B0\u5EFA\uFF0C\u6216\u5148\u5728\u8BDD\u9898\u91CC\u53D1\u6D88\u606F\u521B\u5EFA\u4E00\u4E2A\u3002");
148891
+ return;
148892
+ }
148893
+ const query = args2.trim();
148894
+ let index2 = 0;
148895
+ if (query) {
148896
+ if (!/^\d+$/.test(query)) {
148897
+ await ctx.reply(message, "\u7528\u6CD5\uFF1A`/resume [\u5E8F\u53F7]`\uFF08\u5E8F\u53F7\u89C1 `/sessions`\uFF1B\u7701\u7565 = \u6700\u8FD1\u66F4\u65B0\u7684\u4F1A\u8BDD\uFF09\u3002");
147943
148898
  return;
147944
148899
  }
147945
- if (tracked && !tracked.resolved) {
147946
- await this.patchResolved(tracked, outcome, requestID);
147947
- }
148900
+ index2 = Number.parseInt(query, 10) - 1;
147948
148901
  }
147949
- async patchResolved(tracked, outcome, requestID) {
147950
- tracked.resolved = true;
147951
- const res = await this.deps.sender.patchCard(tracked.messageId, buildResolvedCard(tracked.input, outcome));
147952
- if (!res.ok) this.deps.log.warn("\u5BA1\u6279\u7ED3\u679C\u5361\u7247\u66F4\u65B0\u5931\u8D25", { requestID, error: res.error ?? "unknown" });
148902
+ if (index2 < 0 || index2 >= entries3.length) {
148903
+ await ctx.reply(
148904
+ message,
148905
+ `\u5E8F\u53F7\u8D8A\u754C\uFF1A\u5F53\u524D\u5171 ${entries3.length} \u4E2A\u4F1A\u8BDD\uFF0C\u6709\u6548\u8303\u56F4 1\u2013${entries3.length}\u3002`
148906
+ );
148907
+ return;
148908
+ }
148909
+ const entry = entries3[index2];
148910
+ const result3 = await enterSessionThread(ctx, {
148911
+ chatId: message.chatId,
148912
+ sessionID: entry.sessionID,
148913
+ anchorMessageId: message.messageId,
148914
+ operatorOpenId: message.senderOpenId,
148915
+ info: entry,
148916
+ source: "resume"
148917
+ });
148918
+ if (!result3.ok) {
148919
+ await ctx.reply(
148920
+ message,
148921
+ `\u26A0\uFE0F \u8FDB\u5165\u8BDD\u9898\u5931\u8D25\uFF1A${result3.error ?? "unknown"}\u3002\u53EF\u53D1\u9001 \`/sessions\` \u624B\u52A8\u8FDB\u5165\u3002`
148922
+ );
147953
148923
  }
147954
- };
147955
- function toast2(type, content) {
147956
- return { toast: { type, content } };
147957
148924
  }
147958
- var MAX_ROOT_SUBDIRS = 15;
147959
- async function scanRootSubdirs(root, deps = {}) {
147960
- const rootPath = root.trim();
147961
- if (!rootPath) return [];
147962
- const rd = deps.readdir ?? ((path2, options2) => readdir(path2, options2));
147963
- let entries3;
147964
- try {
147965
- entries3 = await rd(rootPath, { withFileTypes: true });
147966
- } catch {
147967
- return [];
148925
+ async function cmdNow(ctx, message, scope3) {
148926
+ const sessionID = await ctx.scopeSessionID(message, scope3);
148927
+ if (!sessionID) {
148928
+ await ctx.reply(message, "\u5F53\u524D\u6CA1\u6709\u4F1A\u8BDD\u3002");
148929
+ return;
147968
148930
  }
147969
- const names = entries3.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).slice(0, MAX_ROOT_SUBDIRS);
147970
- const out = [];
147971
- for (const name of names) {
147972
- const path2 = join(rootPath, name);
147973
- out.push({ name, path: path2, isRepo: await hasGit(path2, rd) });
148931
+ if (!ctx.deps.promoteQueued) {
148932
+ await ctx.reply(message, "\u5F53\u524D\u7248\u672C\u4E0D\u652F\u6301\u63D2\u961F\uFF08\u8FD0\u884C\u65F6\u672A\u66B4\u9732 inbox\uFF09\u3002");
148933
+ return;
147974
148934
  }
147975
- return out;
148935
+ const promoted = await ctx.deps.promoteQueued(sessionID);
148936
+ if (promoted < 0) {
148937
+ await ctx.reply(message, "\u5F53\u524D\u7248\u672C\u4E0D\u652F\u6301\u63D2\u961F\uFF08\u8FD0\u884C\u65F6\u672A\u66B4\u9732 inbox\uFF09\u3002");
148938
+ return;
148939
+ }
148940
+ if (promoted === 0) {
148941
+ await ctx.reply(message, "\u6CA1\u6709\u6392\u961F\u4E2D\u7684\u6D88\u606F\uFF08\u8BE5\u4F1A\u8BDD\u5F53\u524D\u7A7A\u95F2\u6216\u65E0\u5F85\u6295\u9012\u9879\uFF09\u3002");
148942
+ return;
148943
+ }
148944
+ await ctx.reply(message, `\u26A1 \u5DF2\u628A ${promoted} \u6761\u6392\u961F\u6D88\u606F\u6539\u4E3A\u7ACB\u5373\u63D2\u961F\u6267\u884C\u3002`);
147976
148945
  }
147977
- async function hasGit(path2, rd) {
147978
- try {
147979
- const entries3 = await rd(path2, { withFileTypes: true });
147980
- return entries3.some((e) => e.name === ".git");
147981
- } catch {
147982
- return false;
148946
+ async function cmdSteer(ctx, message, args2, scope3) {
148947
+ const sessionID = await ctx.scopeSessionID(message, scope3);
148948
+ if (!sessionID) {
148949
+ await ctx.reply(message, "\u5F53\u524D\u6CA1\u6709\u4F1A\u8BDD\u3002");
148950
+ return;
148951
+ }
148952
+ const text = args2.trim();
148953
+ if (!text) {
148954
+ await ctx.reply(message, "\u7528\u6CD5\uFF1A`/steer <\u6587\u672C>`\uFF08\u7ACB\u5373\u63D2\u961F\u53D1\u9001\uFF09\uFF1B\u628A\u5DF2\u6392\u961F\u6D88\u606F\u63D2\u961F\u8BF7\u7528 `/now`\u3002");
148955
+ return;
148956
+ }
148957
+ if (!ctx.deps.steerPrompt) {
148958
+ await ctx.reply(message, "\u5F53\u524D\u7248\u672C\u4E0D\u652F\u6301\u63D2\u961F\u3002");
148959
+ return;
147983
148960
  }
148961
+ await ctx.deps.steerPrompt(message, sessionID, text);
148962
+ }
148963
+ function createSessionOpsApi(ctx) {
148964
+ return {
148965
+ cmdUse: (message, args2) => cmdUse(ctx, message, args2),
148966
+ cmdCurrent: (message, scope3) => cmdCurrent(ctx, message, scope3),
148967
+ cmdStop: (message, scope3) => cmdStop(ctx, message, scope3),
148968
+ cmdCd: (message, args2) => cmdCd(ctx, message, args2),
148969
+ cmdResume: (message, args2) => cmdResume(ctx, message, args2),
148970
+ cmdNow: (message, scope3) => cmdNow(ctx, message, scope3),
148971
+ cmdSteer: (message, args2, scope3) => cmdSteer(ctx, message, args2, scope3)
148972
+ };
147984
148973
  }
147985
148974
 
147986
148975
  // src/session-commands.ts
147987
148976
  var SessionCommands = class {
148977
+ ctx;
147988
148978
  constructor(deps) {
147989
- this.deps = deps;
147990
- this.now = deps.now ?? (() => Date.now());
147991
- this.threadRouting = deps.threadRouting ?? true;
147992
- this.modelPageSize = deps.modelPageSize ?? 8;
147993
- this.sessionPageSize = clampPageSize(deps.sessionPageSize);
148979
+ const ctx = createSessionContext(deps);
148980
+ Object.assign(
148981
+ ctx,
148982
+ createSessionListApi(ctx),
148983
+ createSetupWizardApi(ctx),
148984
+ createSessionOpsApi(ctx),
148985
+ createModelPermApi(ctx)
148986
+ );
148987
+ this.ctx = ctx;
147994
148988
  }
147995
- deps;
147996
- now;
147997
- threadRouting;
147998
- modelPageSize;
147999
- sessionPageSize;
148000
148989
  /**
148001
148990
  * 文本命令拦截。返回 true 表示已作为命令处理(调用方**不得**再发 prompt)。
148002
148991
  * 非命令返回 false。
@@ -148007,13 +148996,13 @@ var SessionCommands = class {
148007
148996
  const scope3 = message.threadId ? "thread" : "main";
148008
148997
  try {
148009
148998
  if (scope3 === "thread" && !isCommandAllowedInThread(parsed.name)) {
148010
- await this.reply(message, threadForbiddenText(parsed.raw));
148999
+ await this.ctx.reply(message, threadForbiddenText(parsed.raw));
148011
149000
  return true;
148012
149001
  }
148013
149002
  await this.dispatch(parsed, message, scope3);
148014
149003
  } catch (err2) {
148015
- this.deps.log.warn("\u4F1A\u8BDD\u547D\u4EE4\u5904\u7406\u5931\u8D25", { command: parsed.raw, error: errorMessage(err2) });
148016
- await this.reply(message, `\u26A0\uFE0F \u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF1A${errorMessage(err2)}`);
149004
+ this.ctx.deps.log.warn("\u4F1A\u8BDD\u547D\u4EE4\u5904\u7406\u5931\u8D25", { command: parsed.raw, error: errorMessage(err2) });
149005
+ await this.ctx.reply(message, `\u26A0\uFE0F \u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF1A${errorMessage(err2)}`);
148017
149006
  }
148018
149007
  return true;
148019
149008
  }
@@ -148025,1105 +149014,272 @@ var SessionCommands = class {
148025
149014
  * gateway 的 SDK 会 await 该 Promise 作为回调响应,仍应在 3s 内完成。
148026
149015
  */
148027
149016
  handleCardAction(action) {
149017
+ const ctx = this.ctx;
148028
149018
  if (isSetupFormAction(action.rawValue) || parseSetupFormValues(action.formValue)) {
148029
- if (!this.deps.isAllowed(action.operatorOpenId)) {
148030
- this.deps.log.warn("\u62D2\u7EDD\u975E\u767D\u540D\u5355\u7528\u6237\u7684\u8868\u5355\u63D0\u4EA4", { operator: action.operatorOpenId.slice(0, 8) });
149019
+ if (!ctx.deps.isAllowed(action.operatorOpenId)) {
149020
+ ctx.deps.log.warn("\u62D2\u7EDD\u975E\u767D\u540D\u5355\u7528\u6237\u7684\u8868\u5355\u63D0\u4EA4", { operator: action.operatorOpenId.slice(0, 8) });
148031
149021
  return toast3("error", "\u65E0\u64CD\u4F5C\u6743\u9650");
148032
149022
  }
148033
- void this.applySetupFormSubmit(action).catch((err2) => {
148034
- this.deps.log.warn("\u8868\u5355\u63D0\u4EA4\u5904\u7406\u5931\u8D25", { error: errorMessage(err2) });
149023
+ void ctx.applySetupFormSubmit(action).catch((err2) => {
149024
+ ctx.deps.log.warn("\u8868\u5355\u63D0\u4EA4\u5904\u7406\u5931\u8D25", { error: errorMessage(err2) });
148035
149025
  });
148036
149026
  return toast3("success", "\u6B63\u5728\u521B\u5EFA\u4F1A\u8BDD\u2026");
148037
149027
  }
148038
149028
  const sessionValue = parseSessionCardValue(action.rawValue);
148039
149029
  const setupValue = sessionValue ? void 0 : parseSetupCardValue(action.rawValue);
148040
149030
  if (!sessionValue && !setupValue) return toast3("error", "\u65E0\u6CD5\u8BC6\u522B\u7684\u64CD\u4F5C");
148041
- if (!this.deps.isAllowed(action.operatorOpenId)) {
148042
- this.deps.log.warn("\u62D2\u7EDD\u975E\u767D\u540D\u5355\u7528\u6237\u7684\u5361\u7247\u70B9\u51FB", { operator: action.operatorOpenId.slice(0, 8) });
149031
+ if (!ctx.deps.isAllowed(action.operatorOpenId)) {
149032
+ ctx.deps.log.warn("\u62D2\u7EDD\u975E\u767D\u540D\u5355\u7528\u6237\u7684\u5361\u7247\u70B9\u51FB", { operator: action.operatorOpenId.slice(0, 8) });
148043
149033
  return toast3("error", "\u65E0\u64CD\u4F5C\u6743\u9650");
148044
149034
  }
148045
149035
  if (sessionValue) {
148046
149036
  if (sessionValue.cmd === "open") {
148047
- return this.handleOpenCardAction(action, sessionValue);
149037
+ return ctx.handleOpenCardAction(action, sessionValue);
148048
149038
  }
148049
- void this.applySessionCardAction(action, sessionValue).catch((err2) => {
148050
- this.deps.log.warn("\u4F1A\u8BDD\u5361\u7247\u64CD\u4F5C\u5931\u8D25", { cmd: sessionValue.cmd, error: errorMessage(err2) });
149039
+ void ctx.applySessionCardAction(action, sessionValue).catch((err2) => {
149040
+ ctx.deps.log.warn("\u4F1A\u8BDD\u5361\u7247\u64CD\u4F5C\u5931\u8D25", { cmd: sessionValue.cmd, error: errorMessage(err2) });
148051
149041
  });
148052
149042
  if (sessionValue.cmd === "new") return toast3("success", "\u6B63\u5728\u6253\u5F00\u8868\u5355\u2026");
148053
149043
  if (sessionValue.cmd === "list") return toast3("info", "\u5DF2\u7FFB\u9875");
148054
149044
  return toast3("success", "\u5DF2\u5207\u6362");
148055
149045
  }
148056
149046
  const value3 = setupValue;
148057
- void this.applySetupCardAction(action, value3).catch((err2) => {
148058
- this.deps.log.warn("\u5411\u5BFC\u5361\u7247\u64CD\u4F5C\u5931\u8D25", { wizard: value3.kind, error: errorMessage(err2) });
149047
+ void ctx.applySetupCardAction(action, value3).catch((err2) => {
149048
+ ctx.deps.log.warn("\u5411\u5BFC\u5361\u7247\u64CD\u4F5C\u5931\u8D25", { wizard: value3.kind, error: errorMessage(err2) });
148059
149049
  });
148060
149050
  return setupToast(value3);
148061
149051
  }
148062
- /**
148063
- * `{cmd:"open"}`:白名单(上层已校验)→ `ctx.session.get` 校验存在 →
148064
- * 不存在 toast「会话不存在」并 patch 提示;存在则后台开话题并绑定。
148065
- */
148066
- async handleOpenCardAction(action, value3) {
148067
- if (!this.threadRouting) {
148068
- return toast3("error", "\u5F53\u524D\u4E3A\u56DE\u9000\u6A21\u5F0F\uFF0C\u4E0D\u652F\u6301\u8BDD\u9898\u8DEF\u7531");
148069
- }
148070
- const chatId = action.chatId || value3.chatId;
148071
- const hasApi = Boolean(this.deps.getSessionInfo);
148072
- const info = hasApi ? normalizeSessionInfo(await this.deps.getSessionInfo(value3.sessionID)) : void 0;
148073
- if (hasApi && !info) {
148074
- this.deps.log.warn("\u8FDB\u5165\u8BDD\u9898\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E0D\u5B58\u5728", { sessionID: value3.sessionID, chatId });
148075
- void this.patchMissingCard(action.messageId, value3.sessionID).catch((err2) => {
148076
- this.deps.log.warn("\u4F1A\u8BDD\u4E0D\u5B58\u5728\u63D0\u793A\u5361 patch \u5931\u8D25", { error: errorMessage(err2) });
148077
- });
148078
- return toast3("error", "\u4F1A\u8BDD\u4E0D\u5B58\u5728");
148079
- }
148080
- const snapshot2 = info ?? { sessionID: value3.sessionID, title: "", updatedAt: 0 };
148081
- void this.enterSessionThread({
148082
- chatId,
148083
- sessionID: value3.sessionID,
148084
- anchorMessageId: action.messageId,
148085
- operatorOpenId: action.operatorOpenId,
148086
- info: snapshot2,
148087
- patchMessageId: action.messageId,
148088
- source: "card"
148089
- }).catch((err2) => {
148090
- this.deps.log.warn("\u8FDB\u5165\u8BDD\u9898\u5904\u7406\u5931\u8D25", { sessionID: value3.sessionID, error: errorMessage(err2) });
148091
- });
148092
- return toast3("success", "\u6B63\u5728\u8FDB\u5165\u8BDD\u9898\u2026");
148093
- }
148094
- /** 会话不存在:把原列表卡 patch 成提示卡。 */
148095
- async patchMissingCard(messageId, sessionID) {
148096
- if (!messageId) return;
148097
- await this.deps.sender.patchCard(messageId, buildSessionMissingCard(sessionID));
148098
- }
148099
- /**
148100
- * 「进入话题」核心流程(卡片动作 / `/resume` 共用):
148101
- * `reply_in_thread` 对触发消息开新话题 → 发「✅ 已进入会话」卡 → 拿 thread_id 绑定
148102
- * thread + root 映射。成功后该话题内消息即续上历史会话(opencode 上下文天然持久)。
148103
- */
148104
- async enterSessionThread(input) {
148105
- const { chatId, sessionID, anchorMessageId, operatorOpenId } = input;
148106
- if (!anchorMessageId || !chatId) {
148107
- return { ok: false, error: "missing anchor/chat" };
148108
- }
148109
- const info = input.info;
148110
- const card = buildSessionOpenedCard({
148111
- title: info?.title ?? "",
148112
- sessionID,
148113
- ...info?.directory ? { dir: info.directory } : {},
148114
- ...info?.updatedAt ? { updatedAt: info.updatedAt } : {},
148115
- now: this.now()
148116
- });
148117
- const res = await this.deps.sender.replyCard(anchorMessageId, card, { replyInThread: true });
148118
- this.deps.log.info("\u8FDB\u5165\u4F1A\u8BDD\u5E76\u5F00\u8BDD\u9898", {
148119
- sessionID,
148120
- source: input.source,
148121
- anchorMessageId,
148122
- replyOk: res.ok,
148123
- replyMessageId: res.messageId,
148124
- replyThreadId: res.threadId,
148125
- replyError: res.error
148126
- });
148127
- if (!res.ok || !res.messageId) {
148128
- const error = res.error ?? "unknown";
148129
- this.deps.log.warn("\u8FDB\u5165\u8BDD\u9898\u5931\u8D25", { sessionID, anchorMessageId, error });
148130
- if (input.patchMessageId) {
148131
- await this.deps.sender.patchCard(
148132
- input.patchMessageId,
148133
- buildSessionMissingCard(sessionID, error)
148134
- );
148135
- }
148136
- return { ok: false, error };
148137
- }
148138
- const meta = res.threadId ? void 0 : await this.deps.sender.getMessageMeta(res.messageId);
148139
- const threadId = res.threadId ?? meta?.threadId;
148140
- if (!threadId) {
148141
- this.deps.log.warn("\u8FDB\u5165\u8BDD\u9898\u540E\u672A\u8BFB\u5230 thread_id\uFF0C\u8BE5\u4F1A\u8BDD\u6682\u65E0\u6CD5\u81EA\u52A8\u8DEF\u7531", { messageId: res.messageId });
148142
- return { ok: false, error: "\u672A\u62FF\u5230\u8BDD\u9898 ID" };
148143
- }
148144
- await this.deps.sessionMap.bindThread(threadId, sessionID, chatId, operatorOpenId, anchorMessageId);
148145
- await this.deps.sessionMap.bindRoot(res.messageId, sessionID);
148146
- this.deps.log.info("\u5DF2\u7ED1\u5B9A\u8BDD\u9898\u4E0E\u4F1A\u8BDD", { sessionID, threadId, rootId: res.messageId });
148147
- return { ok: true, threadId };
148148
- }
148149
149052
  async dispatch(parsed, message, scope3) {
148150
149053
  switch (parsed.name) {
148151
149054
  case "new":
148152
- return this.cmdNew(message, parsed.args);
149055
+ return this.ctx.cmdNew(message, parsed.args);
148153
149056
  case "sessions":
148154
- return this.cmdSessions(message);
149057
+ return this.ctx.cmdSessions(message);
148155
149058
  case "use":
148156
- return this.cmdUse(message, parsed.args);
149059
+ return this.ctx.cmdUse(message, parsed.args);
148157
149060
  case "resume":
148158
- return this.cmdResume(message, parsed.args);
149061
+ return this.ctx.cmdResume(message, parsed.args);
148159
149062
  case "current":
148160
- return this.cmdCurrent(message, scope3);
149063
+ return this.ctx.cmdCurrent(message, scope3);
148161
149064
  case "stop":
148162
- return this.cmdStop(message, scope3);
149065
+ return this.ctx.cmdStop(message, scope3);
148163
149066
  case "steer":
148164
- return this.cmdSteer(message, parsed.args, scope3);
149067
+ return this.ctx.cmdSteer(message, parsed.args, scope3);
148165
149068
  case "now":
148166
- return this.cmdNow(message, scope3);
149069
+ return this.ctx.cmdNow(message, scope3);
148167
149070
  case "dir":
148168
- return this.cmdDir(message, parsed.args);
149071
+ return this.ctx.cmdDir(message, parsed.args);
148169
149072
  case "model":
148170
- return this.cmdModel(message, parsed.args, scope3);
149073
+ return this.ctx.cmdModel(message, parsed.args, scope3);
148171
149074
  case "perm":
148172
- return this.cmdPerm(message, parsed.args, scope3);
149075
+ return this.ctx.cmdPerm(message, parsed.args, scope3);
148173
149076
  case "cd":
148174
- return this.cmdCd(message, parsed.args);
149077
+ return this.ctx.cmdCd(message, parsed.args);
148175
149078
  case "cancel":
148176
- return this.cmdCancel(message);
149079
+ return this.ctx.cmdCancel(message);
148177
149080
  case "form":
148178
- return this.cmdForm(message, parsed.args);
149081
+ return this.ctx.cmdForm(message, parsed.args);
148179
149082
  case "unknown":
148180
- return this.reply(message, `\u672A\u77E5\u547D\u4EE4 \`/${parsed.raw}\`
149083
+ return this.ctx.reply(message, `\u672A\u77E5\u547D\u4EE4 \`/${parsed.raw}\`
148181
149084
 
148182
149085
  ${helpText(scope3)}`);
148183
149086
  case "help":
148184
149087
  default:
148185
- return this.reply(message, helpText(scope3));
148186
- }
148187
- }
148188
- // ── 建会话(主聊天流) ────────────────────────────────────────────────
148189
- /**
148190
- * `/new`:与 `/form` **完全等价**,直接发建会话表单卡(不再走目录→模型→权限→确认分步卡)。
148191
- * 带标题时写入向导状态,表单提交后作为会话标题。
148192
- */
148193
- async cmdNew(message, args2) {
148194
- const title = args2.trim() || void 0;
148195
- if (!this.threadRouting) {
148196
- const finalTitle = title ?? defaultSessionTitle(this.now());
148197
- const created = await this.deps.createSession({
148198
- title: finalTitle,
148199
- chatId: message.chatId,
148200
- openId: message.senderOpenId
148201
- });
148202
- await this.reply(message, `\u2705 \u5DF2\u65B0\u5EFA\u5E76\u5207\u6362\u5230\u4F1A\u8BDD\u300C${finalTitle}\u300D
148203
- \`${created.id}\``);
148204
- return;
148205
- }
148206
- await this.openSetupForm(message, title);
148207
- }
148208
- /**
148209
- * `/form [标题]`:直接打开发建会话表单卡。
148210
- * 与 `/new [标题]` 走同一入口,二者完全等价。
148211
- */
148212
- async cmdForm(message, args2) {
148213
- await this.openSetupForm(message, args2.trim() || void 0);
148214
- }
148215
- /**
148216
- * `/new` / `/form` 共同入口:发(或复用)建会话表单卡。
148217
- * 已有向导状态时保留 `/dir` `/model` `/perm` 预填的字段;带标题则更新标题。
148218
- */
148219
- async openSetupForm(message, title) {
148220
- if (message.threadId) {
148221
- await this.reply(message, threadForbiddenText("form"));
148222
- return;
148223
- }
148224
- if (!this.threadRouting) {
148225
- await this.reply(message, "\u5F53\u524D\u4E3A\u56DE\u9000\u6A21\u5F0F\uFF08`threadRouting=false`\uFF09\uFF0C\u4E0D\u652F\u6301\u8868\u5355\u5EFA\u4F1A\u8BDD\uFF0C\u8BF7\u7528 `/new`\u3002");
148226
- return;
148227
- }
148228
- let state = await this.deps.wizard.get(message.chatId);
148229
- if (!state) {
148230
- state = await this.deps.wizard.start(message.chatId, title, message.messageId);
148231
- } else if (title !== void 0) {
148232
- state = { ...state, title };
148233
- await this.deps.wizard.set(message.chatId, state);
148234
- }
148235
- const card = await this.renderFormCard(state);
148236
- const res = await this.deps.sender.sendCard(message.chatId, card);
148237
- if (!res.ok) this.deps.log.warn("\u8868\u5355\u5361\u53D1\u9001\u5931\u8D25", { chatId: message.chatId, error: res.error ?? "unknown" });
148238
- }
148239
- /**
148240
- * `/dir <path>`:目录只作表单**预填**(不再是必经步骤)。
148241
- * 目录容错:留空 = 允许根目录;不存在则自动创建(仍在 allowedRoots 之下)。
148242
- */
148243
- async cmdDir(message, args2) {
148244
- if (message.threadId) {
148245
- await this.reply(message, threadForbiddenText("dir"));
148246
- return;
148247
- }
148248
- const validation = this.deps.validateDir(args2);
148249
- if (!validation.ok) {
148250
- await this.reply(message, validation.message);
148251
- return;
148252
- }
148253
- if (!await this.deps.wizard.get(message.chatId)) {
148254
- await this.deps.wizard.start(message.chatId, void 0, message.messageId);
148255
- }
148256
- const state = await this.deps.wizard.apply(message.chatId, { type: "setDir", dir: validation.path });
148257
- await this.deps.recent.addDir(validation.path);
148258
- if (!state) {
148259
- await this.reply(message, "\u5411\u5BFC\u72B6\u6001\u5DF2\u4E22\u5931\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001 `/new` \u5F00\u59CB\u3002");
148260
- return;
148261
- }
148262
- await this.deps.sender.sendCard(message.chatId, await this.renderFormCard(state));
148263
- }
148264
- /** `/model [关键词]`:向导内选模型(仅预填表单);话题内切换当前会话模型。 */
148265
- async cmdModel(message, args2, scope3) {
148266
- if (scope3 === "thread") {
148267
- const sessionID = await this.threadSessionID(message);
148268
- if (!sessionID) {
148269
- await this.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\u3002\u8BF7\u56DE\u5230\u4E3B\u804A\u5929\u6D41\u7528 `/new` \u65B0\u5EFA\u3002");
148270
- return;
148271
- }
148272
- if (!args2.trim()) {
148273
- await this.sendModelCardToThread(message, sessionID, 0);
148274
- return;
148275
- }
148276
- const models2 = await this.loadModels();
148277
- const matched2 = matchModel(args2, models2);
148278
- if (!matched2.ok) {
148279
- await this.reply(message, modelMatchErrorText(matched2.reason, matched2.candidates));
148280
- return;
148281
- }
148282
- try {
148283
- const outcome = await this.deps.switchSessionModel(sessionID, matched2.model);
148284
- await this.deps.recent.addModel(matched2.model);
148285
- await this.reply(message, modelSwitchReply(outcome, matched2.model));
148286
- } catch (err2) {
148287
- this.deps.log.warn("\u5207\u6362\u4F1A\u8BDD\u6A21\u578B\u5931\u8D25", { sessionID, error: errorMessage(err2) });
148288
- await this.reply(message, `\u26A0\uFE0F \u5207\u6362\u6A21\u578B\u5931\u8D25\uFF1A${errorMessage(err2)}`);
148289
- }
148290
- return;
148291
- }
148292
- if (!await this.deps.wizard.get(message.chatId)) {
148293
- await this.deps.wizard.start(message.chatId, void 0, message.messageId);
148294
- }
148295
- if (!args2.trim()) {
148296
- const state = await this.deps.wizard.get(message.chatId);
148297
- await this.deps.sender.sendCard(message.chatId, await this.renderFormCard(state));
148298
- return;
148299
- }
148300
- const models = await this.loadModels();
148301
- const matched = matchModel(args2, models);
148302
- if (!matched.ok) {
148303
- await this.reply(message, modelMatchErrorText(matched.reason, matched.candidates));
148304
- return;
148305
- }
148306
- const next = await this.deps.wizard.apply(message.chatId, { type: "setModel", model: matched.model });
148307
- await this.deps.recent.addModel(matched.model);
148308
- if (!next) return;
148309
- await this.deps.sender.sendCard(message.chatId, await this.renderFormCard(next));
148310
- }
148311
- /** `/perm [档位]`:向导内选权限(仅预填表单);话题内修改当前会话权限。 */
148312
- async cmdPerm(message, args2, scope3) {
148313
- const arg = args2.trim();
148314
- if (scope3 === "thread") {
148315
- const sessionID = await this.threadSessionID(message);
148316
- if (!sessionID) {
148317
- await this.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\u3002\u8BF7\u56DE\u5230\u4E3B\u804A\u5929\u6D41\u7528 `/new` \u65B0\u5EFA\u3002");
148318
- return;
148319
- }
148320
- if (!arg) {
148321
- await this.sendPermCardToThread(message, sessionID);
148322
- return;
148323
- }
148324
- if (!isPermissionPreset(arg)) {
148325
- await this.reply(message, permUsageText());
148326
- return;
148327
- }
148328
- await this.deps.applyPermissionPreset(sessionID, arg);
148329
- await this.reply(message, `\u2705 \u5DF2\u66F4\u65B0\u672C\u4F1A\u8BDD\u6743\u9650\uFF1A${presetLabel(arg)}`);
148330
- return;
148331
- }
148332
- if (!await this.deps.wizard.get(message.chatId)) {
148333
- await this.deps.wizard.start(message.chatId, void 0, message.messageId);
148334
- }
148335
- if (!arg) {
148336
- const state = await this.deps.wizard.get(message.chatId);
148337
- await this.deps.sender.sendCard(message.chatId, await this.renderFormCard(state));
148338
- return;
148339
- }
148340
- if (!isPermissionPreset(arg)) {
148341
- await this.reply(message, permUsageText());
148342
- return;
148343
- }
148344
- const next = await this.deps.wizard.apply(message.chatId, { type: "setPerm", perm: arg });
148345
- if (!next) return;
148346
- await this.deps.sender.sendCard(message.chatId, await this.renderFormCard(next));
148347
- }
148348
- /**
148349
- * `/cd <path>`:话题内移动当前会话目录。
148350
- * 目录容错:留空 = 回到允许根目录;不存在则自动创建(仍在 allowedRoots 之下)。
148351
- */
148352
- async cmdCd(message, args2) {
148353
- if (!message.threadId) {
148354
- await this.reply(message, "`/cd` \u53EA\u80FD\u5728\u8BDD\u9898\u5185\u4F7F\u7528\uFF08\u7528\u4E8E\u79FB\u52A8\u8BE5\u8BDD\u9898\u4F1A\u8BDD\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF09\u3002");
148355
- return;
148356
- }
148357
- const sessionID = await this.threadSessionID(message);
148358
- if (!sessionID) {
148359
- await this.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\u3002\u8BF7\u56DE\u5230\u4E3B\u804A\u5929\u6D41\u7528 `/new` \u65B0\u5EFA\u3002");
148360
- return;
148361
- }
148362
- const validation = this.deps.validateDir(args2);
148363
- if (!validation.ok) {
148364
- await this.reply(message, validation.message);
148365
- return;
148366
- }
148367
- await this.deps.moveSessionDir(sessionID, validation.path);
148368
- await this.deps.recent.addDir(validation.path);
148369
- await this.reply(message, `\u2705 \u5DF2\u5207\u6362\u672C\u4F1A\u8BDD\u76EE\u5F55\uFF1A\`${validation.path}\``);
148370
- }
148371
- /** `/cancel`:放弃向导。 */
148372
- async cmdCancel(message) {
148373
- if (message.threadId) {
148374
- await this.reply(message, threadForbiddenText("cancel"));
148375
- return;
148376
- }
148377
- await this.deps.wizard.cancel(message.chatId);
148378
- await this.reply(message, "\u2716\uFE0F \u5DF2\u53D6\u6D88\u5EFA\u4F1A\u8BDD\u8868\u5355\u3002\u53D1\u9001 `/new [\u6807\u9898]` \u53EF\u91CD\u65B0\u5F00\u59CB\u3002");
148379
- }
148380
- // ── 既有会话管理 ──────────────────────────────────────────────────────
148381
- /**
148382
- * `/sessions`(别名 `/ls`):列出 opencode **全部**会话(按最近更新倒序、分页 8 条)。
148383
- * 数据源优先 `ctx.session.list()`;拿不到时回退 SessionMap 映射表。
148384
- */
148385
- async cmdSessions(message, page = 0) {
148386
- const entries3 = await this.loadSessionEntries(message.chatId);
148387
- const active = await this.deps.sessionMap.getActive(message.chatId);
148388
- const card = await this.buildListCard(message.chatId, entries3, page, active?.sessionID);
148389
- const res = await this.deps.sender.sendCard(message.chatId, card);
148390
- if (!res.ok) {
148391
- this.deps.log.warn("\u4F1A\u8BDD\u5361\u7247\u53D1\u9001\u5931\u8D25", { chatId: message.chatId, error: res.error ?? "unknown" });
149088
+ return this.ctx.reply(message, helpText(scope3));
148392
149089
  }
148393
149090
  }
148394
- /**
148395
- * `/resume [序号]`:对「最近更新」的会话(或列表第 N 个)直接执行「进入话题」流程,
148396
- * 跳过列表卡。序号越界/无会话时提示。序号与 `/sessions` 列表一致(最近更新倒序)。
148397
- */
148398
- async cmdResume(message, args2) {
148399
- if (message.threadId) {
148400
- await this.reply(message, threadForbiddenText("resume"));
148401
- return;
148402
- }
148403
- if (!this.threadRouting) {
148404
- await this.reply(message, "\u5F53\u524D\u4E3A\u56DE\u9000\u6A21\u5F0F\uFF08`threadRouting=false`\uFF09\uFF0C\u4E0D\u652F\u6301\u7EED\u804A\u5386\u53F2\u4F1A\u8BDD\u3002");
148405
- return;
148406
- }
148407
- const entries3 = await this.loadSessionEntries(message.chatId);
148408
- if (entries3.length === 0) {
148409
- await this.reply(message, "\u6CA1\u6709\u53EF\u7EED\u804A\u7684\u4F1A\u8BDD\u3002\u7528 `/new` \u65B0\u5EFA\uFF0C\u6216\u5148\u5728\u8BDD\u9898\u91CC\u53D1\u6D88\u606F\u521B\u5EFA\u4E00\u4E2A\u3002");
148410
- return;
148411
- }
148412
- const query = args2.trim();
148413
- let index2 = 0;
148414
- if (query) {
148415
- if (!/^\d+$/.test(query)) {
148416
- await this.reply(message, "\u7528\u6CD5\uFF1A`/resume [\u5E8F\u53F7]`\uFF08\u5E8F\u53F7\u89C1 `/sessions`\uFF1B\u7701\u7565 = \u6700\u8FD1\u66F4\u65B0\u7684\u4F1A\u8BDD\uFF09\u3002");
148417
- return;
148418
- }
148419
- index2 = Number.parseInt(query, 10) - 1;
148420
- }
148421
- if (index2 < 0 || index2 >= entries3.length) {
148422
- await this.reply(
148423
- message,
148424
- `\u5E8F\u53F7\u8D8A\u754C\uFF1A\u5F53\u524D\u5171 ${entries3.length} \u4E2A\u4F1A\u8BDD\uFF0C\u6709\u6548\u8303\u56F4 1\u2013${entries3.length}\u3002`
148425
- );
148426
- return;
148427
- }
148428
- const entry = entries3[index2];
148429
- const result3 = await this.enterSessionThread({
148430
- chatId: message.chatId,
148431
- sessionID: entry.sessionID,
148432
- anchorMessageId: message.messageId,
148433
- operatorOpenId: message.senderOpenId,
148434
- info: entry,
148435
- source: "resume"
148436
- });
148437
- if (!result3.ok) {
148438
- await this.reply(
148439
- message,
148440
- `\u26A0\uFE0F \u8FDB\u5165\u8BDD\u9898\u5931\u8D25\uFF1A${result3.error ?? "unknown"}\u3002\u53EF\u53D1\u9001 \`/sessions\` \u624B\u52A8\u8FDB\u5165\u3002`
148441
- );
148442
- }
149091
+ };
149092
+ function setupToast(value3) {
149093
+ switch (value3.kind) {
149094
+ case "dir":
149095
+ return toast3("success", "\u5DF2\u9009\u62E9\u76EE\u5F55");
149096
+ case "model":
149097
+ return toast3("success", "\u5DF2\u9009\u62E9\u6A21\u578B");
149098
+ case "perm":
149099
+ return toast3("success", "\u5DF2\u9009\u62E9\u6743\u9650");
149100
+ case "more":
149101
+ return toast3("info", "\u5DF2\u7FFB\u9875");
149102
+ case "confirm":
149103
+ return toast3("success", "\u6B63\u5728\u521B\u5EFA\u4F1A\u8BDD\u2026");
149104
+ case "form":
149105
+ return toast3("info", "\u5DF2\u6253\u5F00\u8868\u5355");
149106
+ case "cancel":
149107
+ default:
149108
+ return toast3("info", "\u5DF2\u53D6\u6D88");
148443
149109
  }
148444
- /**
148445
- * 加载全量会话列表:优先 `ctx.session.list()`(归一化),失败/形状不识别回退 SessionMap。
148446
- * 关键诊断日志:`列出全部会话` / `会话列表回退到 SessionMap`。
148447
- */
148448
- async loadSessionEntries(chatId) {
148449
- if (this.deps.listAllSessions) {
148450
- try {
148451
- const raw = await this.deps.listAllSessions();
148452
- const normalized = normalizeSessionList(raw);
148453
- if (normalized) {
148454
- this.deps.log.info("\u5217\u51FA\u5168\u90E8\u4F1A\u8BDD", { count: normalized.length, source: "session.list" });
148455
- return normalized;
148456
- }
148457
- this.deps.log.warn("session.list \u8FD4\u56DE\u5F62\u72B6\u65E0\u6CD5\u8BC6\u522B\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", { chatId });
148458
- } catch (err2) {
148459
- this.deps.log.warn("session.list \u8C03\u7528\u5931\u8D25\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", {
148460
- chatId,
148461
- error: errorMessage(err2)
148462
- });
148463
- }
148464
- } else {
148465
- this.deps.log.warn("\u8FD0\u884C\u65F6\u672A\u66B4\u9732 session.list\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", { chatId });
148466
- }
148467
- const fallback = fallbackEntries(await this.deps.sessionMap.listSessions(chatId));
148468
- this.deps.log.warn("\u4F1A\u8BDD\u5217\u8868\u56DE\u9000\u5230 SessionMap", { chatId, count: fallback.length });
148469
- return fallback;
148470
- }
148471
- /** 把全量列表切成当前页并构建列表卡(含「已绑话题」标记)。 */
148472
- async buildListCard(chatId, entries3, page, activeID) {
148473
- const pageSize = this.sessionPageSize;
148474
- const pageCount = Math.max(1, Math.ceil(entries3.length / pageSize));
148475
- const safePage = Math.min(Math.max(0, Math.floor(page)), pageCount - 1);
148476
- const slice = entries3.slice(safePage * pageSize, safePage * pageSize + pageSize);
148477
- const rows = [];
148478
- for (let i = 0; i < slice.length; i += 1) {
148479
- const entry = slice[i];
148480
- const bound = Boolean(await this.deps.sessionMap.threadIdForSession(entry.sessionID));
148481
- rows.push({
148482
- index: safePage * pageSize + i + 1,
148483
- sessionID: entry.sessionID,
148484
- title: entry.title,
148485
- updatedAt: entry.updatedAt,
148486
- ...entry.directory ? { directory: entry.directory } : {},
148487
- bound,
148488
- ...entry.sessionID === activeID ? { active: true } : {}
149110
+ }
149111
+
149112
+ // src/runtime/event-router.ts
149113
+ async function routeEvent(event, deps) {
149114
+ const touched = event.data?.sessionID;
149115
+ if (typeof touched === "string") deps.touch(touched);
149116
+ switch (event.type) {
149117
+ case "permission.asked":
149118
+ void deps.onPermissionAsked(event.data).catch((err2) => deps.log.warn("\u5904\u7406 permission.asked \u5931\u8D25", { error: errorMessage(err2) }));
149119
+ break;
149120
+ case "permission.replied":
149121
+ deps.onPermissionReplied(event.data);
149122
+ break;
149123
+ case "form.created":
149124
+ void deps.onFormCreated(event.data).catch((err2) => deps.log.warn("\u5904\u7406 form.created \u5931\u8D25", { error: errorMessage(err2) }));
149125
+ break;
149126
+ case "form.replied":
149127
+ deps.onFormReplied(event.data);
149128
+ break;
149129
+ case "form.cancelled":
149130
+ deps.onFormCancelled(event.data);
149131
+ break;
149132
+ case "session.text.started": {
149133
+ const data = event.data;
149134
+ deps.applyRun(data.sessionID, {
149135
+ type: "text.started",
149136
+ ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
148489
149137
  });
149138
+ break;
148490
149139
  }
148491
- return buildSessionListCard({
148492
- chatId,
148493
- rows,
148494
- page: safePage,
148495
- pageCount,
148496
- total: entries3.length,
148497
- now: this.now()
148498
- });
148499
- }
148500
- async cmdUse(message, args2) {
148501
- const sessions = await this.deps.sessionMap.listSessions(message.chatId);
148502
- if (sessions.length === 0) {
148503
- await this.reply(message, "\u8FD8\u6CA1\u6709\u4F1A\u8BDD\u3002\u4F7F\u7528 /new \u65B0\u5EFA\u4E00\u4E2A\u3002");
148504
- return;
148505
- }
148506
- const matched = matchSession(args2, sessions);
148507
- if (!matched.ok) {
148508
- await this.reply(message, useErrorText(matched.reason));
148509
- return;
148510
- }
148511
- const entry = matched.entry;
148512
- const ok = await this.deps.sessionMap.setActive(message.chatId, entry.sessionID);
148513
- if (!ok) {
148514
- await this.reply(message, "\u5207\u6362\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u5148\u7528 /sessions \u67E5\u770B\u5217\u8868\u3002");
148515
- return;
148516
- }
148517
- await this.reply(message, `\u2705 \u5DF2\u5207\u6362\u5230\u300C${entry.title.trim() || "(\u672A\u547D\u540D)"}\u300D
148518
- \`${entry.sessionID}\``);
148519
- }
148520
- async cmdCurrent(message, scope3) {
148521
- if (scope3 === "thread") {
148522
- const sessionID = await this.threadSessionID(message);
148523
- if (!sessionID) {
148524
- await this.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\u3002\u8BF7\u56DE\u5230\u4E3B\u804A\u5929\u6D41\u7528 `/new` \u65B0\u5EFA\u3002");
148525
- return;
148526
- }
148527
- const entry = await this.deps.sessionMap.getSession(message.chatId, sessionID);
148528
- const link2 = await this.deps.sessionMap.resolveBySession(sessionID);
148529
- const model = await this.currentModel(sessionID) ?? link2?.model;
148530
- const lines = [
148531
- `\u672C\u8BDD\u9898\u4F1A\u8BDD\uFF1A\u300C${entry?.title.trim() || "(\u672A\u547D\u540D)"}\u300D`,
148532
- `\`${sessionID}\``,
148533
- ...link2?.dir ? [`\u76EE\u5F55\uFF1A\`${link2.dir}\``] : [],
148534
- ...model ? [`\u6A21\u578B\uFF1A${modelLabel(model)}`] : [],
148535
- ...link2?.perm ? [`\u6743\u9650\uFF1A${presetLabel(link2.perm)}`] : []
148536
- ];
148537
- await this.reply(message, lines.join("\n"));
148538
- return;
148539
- }
148540
- const active = await this.deps.sessionMap.getActive(message.chatId);
148541
- if (!active) {
148542
- await this.reply(message, "\u5F53\u524D\u6CA1\u6709\u4F1A\u8BDD\u3002\u4F7F\u7528 /new \u65B0\u5EFA\u4E00\u4E2A\u3002");
148543
- return;
148544
- }
148545
- const count = (await this.deps.sessionMap.listSessions(message.chatId)).length;
148546
- await this.reply(
148547
- message,
148548
- `\u5F53\u524D\u4F1A\u8BDD\uFF1A\u300C${active.title.trim() || "(\u672A\u547D\u540D)"}\u300D
148549
- \`${active.sessionID}\`
148550
- \u5171 ${count} \u4E2A\u4F1A\u8BDD\u3002`
148551
- );
148552
- }
148553
- async cmdStop(message, scope3) {
148554
- if (scope3 === "thread") {
148555
- const sessionID = await this.threadSessionID(message);
148556
- if (!sessionID) {
148557
- await this.reply(message, "\u672C\u8BDD\u9898\u5C1A\u672A\u5173\u8054\u4F1A\u8BDD\uFF0C\u65E0\u6CD5\u4E2D\u65AD\u3002");
148558
- return;
148559
- }
148560
- await this.deps.interruptSession(sessionID);
148561
- const entry = await this.deps.sessionMap.getSession(message.chatId, sessionID);
148562
- await this.reply(message, `\u23F9\uFE0F \u5DF2\u8BF7\u6C42\u4E2D\u65AD\u672C\u8BDD\u9898\u4F1A\u8BDD\uFF1A\u300C${entry?.title.trim() || "(\u672A\u547D\u540D)"}\u300D`);
148563
- return;
148564
- }
148565
- const active = await this.deps.sessionMap.getActive(message.chatId);
148566
- if (!active) {
148567
- await this.reply(message, "\u5F53\u524D\u6CA1\u6709\u4F1A\u8BDD\u53EF\u4E2D\u65AD\u3002");
148568
- return;
148569
- }
148570
- await this.deps.interruptSession(active.sessionID);
148571
- await this.reply(message, `\u23F9\uFE0F \u5DF2\u8BF7\u6C42\u4E2D\u65AD\u5F53\u524D\u4F1A\u8BDD\uFF1A\u300C${active.title.trim() || "(\u672A\u547D\u540D)"}\u300D`);
148572
- }
148573
- /** 解析命令作用域内的目标会话:话题内用 thread 映射,主聊天流用当前会话。 */
148574
- async scopeSessionID(message, scope3) {
148575
- if (scope3 === "thread") return this.threadSessionID(message);
148576
- const active = await this.deps.sessionMap.getActive(message.chatId);
148577
- return active?.sessionID;
148578
- }
148579
- /** `/now`:把已排队(未投递)的消息提升为 steer,立即插队执行。 */
148580
- async cmdNow(message, scope3) {
148581
- const sessionID = await this.scopeSessionID(message, scope3);
148582
- if (!sessionID) {
148583
- await this.reply(message, "\u5F53\u524D\u6CA1\u6709\u4F1A\u8BDD\u3002");
148584
- return;
148585
- }
148586
- if (!this.deps.promoteQueued) {
148587
- await this.reply(message, "\u5F53\u524D\u7248\u672C\u4E0D\u652F\u6301\u63D2\u961F\uFF08\u8FD0\u884C\u65F6\u672A\u66B4\u9732 inbox\uFF09\u3002");
148588
- return;
148589
- }
148590
- const promoted = await this.deps.promoteQueued(sessionID);
148591
- if (promoted < 0) {
148592
- await this.reply(message, "\u5F53\u524D\u7248\u672C\u4E0D\u652F\u6301\u63D2\u961F\uFF08\u8FD0\u884C\u65F6\u672A\u66B4\u9732 inbox\uFF09\u3002");
148593
- return;
148594
- }
148595
- if (promoted === 0) {
148596
- await this.reply(message, "\u6CA1\u6709\u6392\u961F\u4E2D\u7684\u6D88\u606F\uFF08\u8BE5\u4F1A\u8BDD\u5F53\u524D\u7A7A\u95F2\u6216\u65E0\u5F85\u6295\u9012\u9879\uFF09\u3002");
148597
- return;
148598
- }
148599
- await this.reply(message, `\u26A1 \u5DF2\u628A ${promoted} \u6761\u6392\u961F\u6D88\u606F\u6539\u4E3A\u7ACB\u5373\u63D2\u961F\u6267\u884C\u3002`);
148600
- }
148601
- /** `/steer <文本>`:立即插队发送一条消息(打断当前步骤插入执行)。 */
148602
- async cmdSteer(message, args2, scope3) {
148603
- const sessionID = await this.scopeSessionID(message, scope3);
148604
- if (!sessionID) {
148605
- await this.reply(message, "\u5F53\u524D\u6CA1\u6709\u4F1A\u8BDD\u3002");
148606
- return;
148607
- }
148608
- const text = args2.trim();
148609
- if (!text) {
148610
- await this.reply(message, "\u7528\u6CD5\uFF1A`/steer <\u6587\u672C>`\uFF08\u7ACB\u5373\u63D2\u961F\u53D1\u9001\uFF09\uFF1B\u628A\u5DF2\u6392\u961F\u6D88\u606F\u63D2\u961F\u8BF7\u7528 `/now`\u3002");
148611
- return;
148612
- }
148613
- if (!this.deps.steerPrompt) {
148614
- await this.reply(message, "\u5F53\u524D\u7248\u672C\u4E0D\u652F\u6301\u63D2\u961F\u3002");
148615
- return;
148616
- }
148617
- await this.deps.steerPrompt(message, sessionID, text);
148618
- }
148619
- // ── 会话列表卡片 ──────────────────────────────────────────────────────
148620
- /**
148621
- * 会话卡按钮(`use` / `new` / `list`;`open` 由 `handleOpenCardAction` 处理)。
148622
- * - `new`:发送 `/form` 建会话表单卡(不再直接建会话);
148623
- * - `list`:按页码 patch 当前列表卡;
148624
- * - `use`:旧卡片兼容,切换当前会话并刷新列表(含翻页位置)。
148625
- */
148626
- async applySessionCardAction(action, value3) {
148627
- const chatId = action.chatId || value3.chatId;
148628
- if (!chatId) return;
148629
- if (value3.cmd === "new") {
148630
- await this.sendSetupFormForChat(chatId, action.messageId, action.operatorOpenId);
148631
- return;
148632
- }
148633
- if (value3.cmd === "list") {
148634
- if (action.messageId) await this.patchListCard(chatId, action.messageId, value3.page);
148635
- return;
148636
- }
148637
- if (value3.cmd === "open") {
148638
- return;
148639
- }
148640
- const ok = await this.deps.sessionMap.setActive(chatId, value3.sessionID);
148641
- if (!ok) this.deps.log.debug("\u5361\u7247\u5207\u6362\u4F1A\u8BDD\u5931\u8D25\uFF1A\u4F1A\u8BDD\u4E0D\u5B58\u5728", { sessionID: value3.sessionID });
148642
- if (action.messageId) await this.patchListCard(chatId, action.messageId);
148643
- }
148644
- /** 列表卡「➕ 新建会话」→ 打开 `/form` 建会话表单卡(复用向导状态/标题)。 */
148645
- async sendSetupFormForChat(chatId, anchorMessageId, openId) {
148646
- if (!this.threadRouting) {
148647
- const title = defaultSessionTitle(this.now());
148648
- await this.deps.createSession({ title, chatId, openId });
148649
- await this.replyChat(chatId, `\u2705 \u5DF2\u65B0\u5EFA\u4F1A\u8BDD\u300C${title}\u300D`);
148650
- return;
148651
- }
148652
- let state = await this.deps.wizard.get(chatId);
148653
- if (!state) state = await this.deps.wizard.start(chatId, void 0, anchorMessageId);
148654
- const card = await this.renderFormCard(state);
148655
- const res = await this.deps.sender.sendCard(chatId, card);
148656
- if (!res.ok) this.deps.log.warn("\u8868\u5355\u5361\u53D1\u9001\u5931\u8D25", { chatId, error: res.error ?? "unknown" });
148657
- }
148658
- async patchListCard(chatId, messageId, page = 0) {
148659
- const entries3 = await this.loadSessionEntries(chatId);
148660
- const active = await this.deps.sessionMap.getActive(chatId);
148661
- const card = await this.buildListCard(chatId, entries3, page, active?.sessionID);
148662
- const res = await this.deps.sender.patchCard(messageId, card);
148663
- if (!res.ok) this.deps.log.warn("\u4F1A\u8BDD\u5361\u7247\u66F4\u65B0\u5931\u8D25", { error: res.error ?? "unknown" });
148664
- }
148665
- /** 主聊天流文本回执(不引用任何消息)。 */
148666
- async replyChat(chatId, text) {
148667
- await this.deps.sender.sendText(chatId, text);
148668
- }
148669
- // ── 向导 / 会话操作卡片 ───────────────────────────────────────────────
148670
- async applySetupCardAction(action, value3) {
148671
- const chatId = action.chatId;
148672
- if (value3.kind === "confirm") {
148673
- await this.confirmSetup(chatId, action);
148674
- return;
148675
- }
148676
- if (value3.kind === "cancel") {
148677
- await this.deps.wizard.cancel(chatId);
148678
- await this.patchCard(action.messageId, buildSetupDoneCard("\u2716\uFE0F \u5DF2\u53D6\u6D88", ["\u5EFA\u4F1A\u8BDD\u5411\u5BFC\u5DF2\u53D6\u6D88\u3002\u53D1\u9001 `/new` \u91CD\u65B0\u5F00\u59CB\u3002"]));
148679
- return;
148680
- }
148681
- if (value3.kind === "form") {
148682
- const state2 = await this.deps.wizard.get(chatId) ?? await this.deps.wizard.start(chatId, void 0, action.messageId);
148683
- await this.patchCard(action.messageId, await this.renderFormCard(state2));
148684
- return;
148685
- }
148686
- if (value3.kind !== "dir" && value3.sid) {
148687
- if (value3.kind === "model") {
148688
- try {
148689
- const outcome = await this.deps.switchSessionModel(value3.sid, value3.model);
148690
- await this.deps.recent.addModel(value3.model);
148691
- await this.patchCard(action.messageId, modelSwitchDoneCard(outcome, value3.model));
148692
- } catch (err2) {
148693
- this.deps.log.warn("\u5361\u7247\u5207\u6362\u6A21\u578B\u5931\u8D25", { sessionID: value3.sid, error: errorMessage(err2) });
148694
- await this.patchCard(
148695
- action.messageId,
148696
- buildSetupDoneCard("\u26A0\uFE0F \u5207\u6362\u6A21\u578B\u5931\u8D25", [errorMessage(err2)])
148697
- );
148698
- }
148699
- return;
148700
- }
148701
- if (value3.kind === "perm") {
148702
- await this.deps.applyPermissionPreset(value3.sid, value3.preset);
148703
- await this.patchCard(
148704
- action.messageId,
148705
- buildSetupDoneCard("\u2705 \u5DF2\u66F4\u65B0\u6743\u9650", [`\u5F53\u524D\u6743\u9650\uFF1A${presetLabel(value3.preset)}`])
148706
- );
148707
- return;
148708
- }
148709
- if (value3.kind === "more") {
148710
- const current = await this.currentModel(value3.sid);
148711
- const models = await this.loadModels();
148712
- const recent = await this.deps.recent.listModels();
148713
- const card = buildModelCard({
148714
- models,
148715
- recent,
148716
- ...current ? { current } : {},
148717
- page: value3.page,
148718
- pageSize: this.modelPageSize,
148719
- recentLimit: this.recentModelsLimit(),
148720
- sid: value3.sid
148721
- });
148722
- await this.patchCard(action.messageId, card);
148723
- return;
148724
- }
148725
- return;
148726
- }
148727
- const state = await this.deps.wizard.get(chatId);
148728
- if (!state) {
148729
- await this.patchCard(action.messageId, buildSetupDoneCard("\u26A0\uFE0F \u5411\u5BFC\u5DF2\u5931\u6548", ["\u8BF7\u91CD\u65B0\u53D1\u9001 `/new` \u5F00\u59CB\u3002"]));
148730
- return;
148731
- }
148732
- if (value3.kind === "dir") {
148733
- const next = await this.deps.wizard.apply(chatId, { type: "setDir", dir: value3.dir });
148734
- await this.deps.recent.addDir(value3.dir);
148735
- if (!next) return;
148736
- await this.patchCard(action.messageId, await this.renderModelCard(next));
148737
- return;
148738
- }
148739
- if (value3.kind === "model") {
148740
- const next = await this.deps.wizard.apply(chatId, { type: "setModel", model: value3.model });
148741
- await this.deps.recent.addModel(value3.model);
148742
- if (!next) return;
148743
- await this.patchCard(action.messageId, buildPermCard({ ...next.perm ? { current: next.perm } : {} }));
148744
- return;
149140
+ case "session.text.delta": {
149141
+ const data = event.data;
149142
+ deps.applyRun(data.sessionID, {
149143
+ type: "text.delta",
149144
+ delta: data.delta,
149145
+ ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
149146
+ });
149147
+ break;
148745
149148
  }
148746
- if (value3.kind === "perm") {
148747
- const next = await this.deps.wizard.apply(chatId, { type: "setPerm", perm: value3.preset });
148748
- if (!next) return;
148749
- await this.patchCard(action.messageId, buildConfirmCard(confirmInput(next)));
148750
- return;
149149
+ case "session.text.ended": {
149150
+ const data = event.data;
149151
+ deps.applyRun(data.sessionID, {
149152
+ type: "text.ended",
149153
+ ...data.text ? { text: data.text } : {},
149154
+ ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
149155
+ });
149156
+ break;
148751
149157
  }
148752
- if (value3.kind === "more") {
148753
- const next = await this.deps.wizard.apply(chatId, { type: "setPage", page: value3.page });
148754
- if (!next) return;
148755
- await this.patchCard(action.messageId, await this.renderModelCard(next, value3.page));
149158
+ case "session.tool.input.started": {
149159
+ const data = event.data;
149160
+ deps.applyRun(data.sessionID, {
149161
+ type: "tool.input.started",
149162
+ id: data.id,
149163
+ name: data.name,
149164
+ ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
149165
+ });
149166
+ break;
148756
149167
  }
148757
- }
148758
- /**
148759
- * 确认卡「✅ 创建」:读向导 → 走统一创建路径。
148760
- *
148761
- * @deprecated `/new` 已不再发确认卡;仅当用户点击**历史遗留**的确认卡时才会走到这里。
148762
- * 新流程见 `applySetupFormSubmit`(表单提交)。
148763
- */
148764
- async confirmSetup(chatId, action) {
148765
- const state = await this.deps.wizard.get(chatId);
148766
- if (!state || state.step !== "confirm" || !state.dir || !state.perm) {
148767
- await this.patchCard(action.messageId, buildSetupDoneCard("\u26A0\uFE0F \u5411\u5BFC\u72B6\u6001\u4E0D\u5B8C\u6574", ["\u8BF7\u91CD\u65B0\u53D1\u9001 `/new` \u5F00\u59CB\u3002"]));
148768
- return;
149168
+ case "session.tool.input.ended": {
149169
+ const data = event.data;
149170
+ deps.applyRun(data.sessionID, { type: "tool.input.ended", id: data.id, input: data.input });
149171
+ break;
148769
149172
  }
148770
- await this.deps.wizard.cancel(chatId);
148771
- const title = state.title?.trim() || defaultSessionTitle(this.now());
148772
- await this.createSessionFromSetup(chatId, action, {
148773
- title,
148774
- dir: state.dir,
148775
- perm: state.perm,
148776
- ...state.model ? { model: state.model } : {},
148777
- ...state.anchorMessageId ? { anchorMessageId: state.anchorMessageId } : {}
148778
- });
148779
- }
148780
- /**
148781
- * 表单提交(P6.1):与按钮向导**共用创建路径**。
148782
- * 目录先用 `validateDir` 校验(失败 → 回带错误说明的表单卡并保留已填项,不建会话)。
148783
- */
148784
- async applySetupFormSubmit(action) {
148785
- const fv = action.formValue;
148786
- const fvKeys = fv && typeof fv === "object" ? Object.keys(fv) : [];
148787
- this.deps.log.info("\u8868\u5355\u63D0\u4EA4\u8FDB\u5165\u5904\u7406", {
148788
- chatId: action.chatId,
148789
- messageId: action.messageId,
148790
- formValueKeys: fvKeys,
148791
- formValueType: Array.isArray(fv) ? "array" : typeof fv
148792
- });
148793
- const values2 = parseSetupFormValues(action.formValue);
148794
- if (!values2) {
148795
- this.deps.log.warn("\u8868\u5355\u6570\u636E\u7F3A\u5931\uFF08parse \u8FD4\u56DE undefined\uFF09", { formValueKeys: fvKeys });
148796
- await this.patchCard(action.messageId, buildSetupDoneCard("\u26A0\uFE0F \u8868\u5355\u6570\u636E\u7F3A\u5931", ["\u8BF7\u91CD\u65B0\u53D1\u9001 `/form` \u586B\u5199\u3002"]));
148797
- return;
149173
+ case "session.tool.success": {
149174
+ const data = event.data;
149175
+ deps.applyRun(data.sessionID, {
149176
+ type: "tool.success",
149177
+ id: data.id,
149178
+ output: contentToText(data.content)
149179
+ });
149180
+ break;
148798
149181
  }
148799
- const state = await this.deps.wizard.get(action.chatId);
148800
- if (!state) {
148801
- this.deps.log.warn("\u8868\u5355\u63D0\u4EA4\u4F46\u5411\u5BFC\u72B6\u6001\u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F/\u5DF2\u88AB\u6D88\u8D39\uFF09", {
148802
- chatId: action.chatId,
148803
- dirLen: values2.dir.length,
148804
- hasModel: Boolean(values2.model),
148805
- hasPerm: Boolean(values2.perm)
149182
+ case "session.tool.error": {
149183
+ const data = event.data;
149184
+ deps.applyRun(data.sessionID, {
149185
+ type: "tool.error",
149186
+ id: data.id,
149187
+ output: extractErrorText(data.error ?? data.content)
148806
149188
  });
148807
- await this.patchCard(action.messageId, buildSetupDoneCard("\u26A0\uFE0F \u8868\u5355\u5DF2\u5931\u6548", ["\u8BF7\u91CD\u65B0\u53D1\u9001 `/form` \u6216 `/new` \u6253\u5F00\u8868\u5355\u3002"]));
148808
- return;
149189
+ break;
148809
149190
  }
148810
- const title = state.title?.trim() || defaultSessionTitle(this.now());
148811
- const requestedDir = resolveSetupFormDir(values2, this.deps.allowedRoots ?? []);
148812
- let preserved = {
148813
- dir: requestedDir,
148814
- ...values2.model ? { model: values2.model } : {},
148815
- ...values2.perm ? { perm: values2.perm } : {}
148816
- };
148817
- this.deps.log.info("\u8868\u5355\u5B57\u6BB5\u89E3\u6790", {
148818
- dirInput: values2.dir,
148819
- dirSelect: values2.dirSelect,
148820
- dir: requestedDir,
148821
- model: values2.model ? `${values2.model.providerID}/${values2.model.id}` : void 0,
148822
- perm: values2.perm,
148823
- hasState: true
148824
- });
148825
- const validation = this.deps.validateDir(requestedDir);
148826
- if (!validation.ok) {
148827
- this.deps.log.warn("\u76EE\u5F55\u6821\u9A8C\u5931\u8D25", { dir: requestedDir, reason: validation.message });
148828
- await this.patchCard(
148829
- action.messageId,
148830
- await this.renderFormCard(state, { error: validation.message, values: preserved })
148831
- );
148832
- return;
149191
+ case "session.execution.started": {
149192
+ const data = event.data;
149193
+ deps.markStarted(data.sessionID);
149194
+ deps.applyRun(data.sessionID, { type: "execution.started" });
149195
+ break;
148833
149196
  }
148834
- preserved = { ...preserved, dir: validation.path };
148835
- if (!values2.perm) {
148836
- this.deps.log.warn("\u6743\u9650\u6863\u4F4D\u7F3A\u5931", { dir: validation.path });
148837
- await this.patchCard(
148838
- action.messageId,
148839
- await this.renderFormCard(state, { error: "\u8BF7\u9009\u62E9\u6743\u9650\u6863\u4F4D\u3002", values: { ...preserved, dir: validation.path } })
148840
- );
148841
- return;
149197
+ case "session.execution.succeeded": {
149198
+ const data = event.data;
149199
+ deps.markEnded(data.sessionID);
149200
+ deps.applyRun(data.sessionID, { type: "execution.succeeded" });
149201
+ break;
148842
149202
  }
148843
- const model = values2.model ? await this.resolveFormModel(values2.model) : state?.model;
148844
- this.deps.log.info("\u8868\u5355\u6821\u9A8C\u901A\u8FC7\uFF0C\u5F00\u59CB\u5EFA\u4F1A\u8BDD", {
148845
- dir: validation.path,
148846
- perm: values2.perm,
148847
- model: model ? `${model.providerID}/${model.id}` : void 0
148848
- });
148849
- await this.deps.wizard.cancel(action.chatId);
148850
- await this.createSessionFromSetup(action.chatId, action, {
148851
- title,
148852
- dir: validation.path,
148853
- perm: values2.perm,
148854
- ...model ? { model } : {}
148855
- });
148856
- }
148857
- /** 表单模型引用 → 尽量补全 name(列表不可用时保留原引用)。 */
148858
- async resolveFormModel(ref) {
148859
- const models = await this.loadModels();
148860
- const matched = matchModel(`${ref.providerID}/${ref.id}`, models);
148861
- return matched.ok ? matched.model : ref;
148862
- }
148863
- /** 建会话统一创建路径(按钮确认 / 表单提交共用)。 */
148864
- async createSessionFromSetup(chatId, action, opts) {
148865
- const { title, dir, perm, model } = opts;
148866
- const permissions = presetToRuleset(perm);
148867
- const gateMode = presetGateMode(perm);
148868
- const created = await this.deps.createSession({
148869
- title,
148870
- chatId,
148871
- openId: action.operatorOpenId,
148872
- setActive: true,
148873
- directory: dir,
148874
- permissions,
148875
- perm,
148876
- gateMode,
148877
- ...model ? { model } : {}
148878
- });
148879
- await this.deps.recent.addDir(dir);
148880
- if (model) await this.deps.recent.addModel(model);
148881
- const readyCard = buildSessionReadyCard({
148882
- title,
148883
- sessionID: created.id,
148884
- dir,
148885
- ...model ? { model: modelLabel(model) } : {},
148886
- perm: presetLabel(perm)
148887
- });
148888
- const anchorId = action.messageId || opts.anchorMessageId || "";
148889
- const res = await this.deps.sender.replyCard(anchorId, readyCard, { replyInThread: true });
148890
- this.deps.log.info("\u521B\u5EFA\u4F1A\u8BDD\u5E76\u5F00\u8BDD\u9898", {
148891
- sessionID: created.id,
148892
- anchorMessageId: anchorId,
148893
- anchorFromFormCard: Boolean(action.messageId),
148894
- replyOk: res.ok,
148895
- replyMessageId: res.messageId,
148896
- replyThreadId: res.threadId,
148897
- replyError: res.error
148898
- });
148899
- if (!res.ok || !res.messageId) {
148900
- this.deps.log.warn("\u4E00\u952E\u5F00\u8BDD\u9898\u5931\u8D25", { error: res.error ?? "unknown" });
148901
- await this.patchCard(
148902
- action.messageId,
148903
- buildSessionCreatedCard({
148904
- title,
148905
- sessionID: created.id,
148906
- dir,
148907
- ...model ? { model: modelLabel(model) } : {},
148908
- perm: presetLabel(perm),
148909
- note: "\u26A0\uFE0F \u81EA\u52A8\u5F00\u8BDD\u9898\u5931\u8D25\uFF1A\u8BF7\u5728 `/sessions` \u7684\u4F1A\u8BDD\u5361\u4E0A\u624B\u52A8\u300C\u521B\u5EFA\u8BDD\u9898\u300D\uFF0C\u6216\u5728\u4E3B\u804A\u5929\u6D41\u7528 `/use` \u5207\u6362\u540E\u7EE7\u7EED\u3002"
148910
- })
148911
- );
148912
- return;
149203
+ case "session.execution.failed": {
149204
+ const data = event.data;
149205
+ deps.markEnded(data.sessionID);
149206
+ deps.applyRun(data.sessionID, { type: "execution.failed", error: extractErrorText(data.error) });
149207
+ void deps.notifyFailure(data.sessionID, data.error);
149208
+ break;
148913
149209
  }
148914
- await this.deps.sessionMap.bindRoot(anchorId, created.id);
148915
- const meta = res.threadId ? void 0 : await this.deps.sender.getMessageMeta(res.messageId);
148916
- const threadId = res.threadId ?? meta?.threadId;
148917
- if (threadId) {
148918
- await this.deps.sessionMap.bindThread(threadId, created.id, chatId, action.operatorOpenId, anchorId);
148919
- } else {
148920
- this.deps.log.warn("\u4E00\u952E\u5F00\u8BDD\u9898\u540E\u672A\u8BFB\u5230 thread_id\uFF0C\u8BE5\u4F1A\u8BDD\u6682\u65E0\u6CD5\u81EA\u52A8\u8DEF\u7531", { messageId: res.messageId });
149210
+ case "session.execution.interrupted": {
149211
+ const data = event.data;
149212
+ deps.markEnded(data.sessionID);
149213
+ deps.applyRun(data.sessionID, { type: "execution.failed", error: `\u5DF2\u4E2D\u65AD\uFF08${data.reason ?? "unknown"}\uFF09` });
149214
+ break;
148921
149215
  }
148922
- await this.patchCard(
148923
- action.messageId,
148924
- buildSessionCreatedCard({
148925
- title,
148926
- sessionID: created.id,
148927
- dir,
148928
- ...model ? { model: modelLabel(model) } : {},
148929
- perm: presetLabel(perm),
148930
- ...threadId ? {} : { note: "\uFF08\u672A\u62FF\u5230\u8BDD\u9898 ID\uFF0C\u82E5\u8BDD\u9898\u672A\u51FA\u73B0\u8BF7\u5728\u4F1A\u8BDD\u5361\u4E0A\u624B\u52A8\u521B\u5EFA\u3002\uFF09" }
148931
- })
148932
- );
148933
- }
148934
- // ── 卡片渲染辅助 ──────────────────────────────────────────────────────
148935
- // 说明:`/new` 已改为直接发建会话表单卡,以下分步卡的发送方法已不再使用(已移除)。
148936
- // 分步卡的**构建函数**(buildDirCard/buildModelCard/buildPermCard/buildConfirmCard)与
148937
- // 对应卡片回调分支仍保留,用于兼容旧卡片与单测(标注 @deprecated)。
148938
- async renderModelCard(state, pageOverride) {
148939
- const models = await this.loadModels();
148940
- const recent = await this.deps.recent.listModels();
148941
- return buildModelCard({
148942
- models,
148943
- recent,
148944
- ...state.model ? { current: state.model } : {},
148945
- page: pageOverride ?? state.page ?? 0,
148946
- pageSize: this.modelPageSize,
148947
- recentLimit: this.recentModelsLimit()
148948
- });
148949
- }
148950
- /**
148951
- * 渲染建会话表单卡(P6.1):最近模型 + 常用模型 + 默认预选。
148952
- * P6.2:目录/权限也从向导状态预填(`/dir` `/perm` 的能力,不再是必经步骤)。
148953
- */
148954
- async renderFormCard(state, over) {
148955
- const models = await this.loadModels();
148956
- const recent = await this.deps.recent.listModels();
148957
- const rootSubdirs = await this.scanRoot(this.deps.allowedRoots?.[0]);
148958
- const values2 = over?.values ?? {
148959
- ...state?.dir ? { dir: state.dir } : {},
148960
- ...state?.perm ? { perm: state.perm } : {}
148961
- };
148962
- return buildSetupFormCard({
148963
- models,
148964
- recent,
148965
- ...state?.model ? { defaultModel: state.model } : {},
148966
- ...rootSubdirs.length > 0 ? { rootSubdirs } : {},
148967
- ...this.deps.allowedRoots ? { allowedRoots: this.deps.allowedRoots } : {},
148968
- ...over?.error ? { error: over.error } : {},
148969
- values: values2
148970
- });
148971
- }
148972
- async sendModelCardToThread(message, sessionID, page) {
148973
- const current = await this.currentModel(sessionID);
148974
- const models = await this.loadModels();
148975
- const recent = await this.deps.recent.listModels();
148976
- const card = buildModelCard({
148977
- models,
148978
- recent,
148979
- ...current ? { current } : {},
148980
- page,
148981
- pageSize: this.modelPageSize,
148982
- recentLimit: this.recentModelsLimit(),
148983
- sid: sessionID
148984
- });
148985
- await this.deps.sender.replyCard(message.messageId, card);
148986
- }
148987
- /**
148988
- * 读回会话**真实**当前模型(优先 `ctx.session.get`),失败则降级到插件记录值。
148989
- * 展示(运行卡页脚 / `/current` / 模型卡)一律以读回值为准,避免只显示我们记录的那份。
148990
- */
148991
- async currentModel(sessionID) {
148992
- const link2 = await this.deps.sessionMap.resolveBySession(sessionID);
148993
- if (this.deps.getSessionModel) {
148994
- try {
148995
- const model = await this.deps.getSessionModel(sessionID, link2?.dir);
148996
- if (model) return model;
148997
- } catch (err2) {
148998
- this.deps.log.warn("\u8BFB\u56DE\u4F1A\u8BDD\u6A21\u578B\u5931\u8D25\uFF0C\u56DE\u9000\u8BB0\u5F55\u503C", { sessionID, error: errorMessage(err2) });
149216
+ case "session.status": {
149217
+ const data = event.data;
149218
+ const statusType = data.status?.type;
149219
+ if (statusType === "idle") {
149220
+ deps.markEnded(data.sessionID);
149221
+ deps.applyRun(data.sessionID, { type: "execution.succeeded" });
149222
+ } else if (statusType === "busy" || statusType === "retry") {
149223
+ deps.markStarted(data.sessionID);
148999
149224
  }
149225
+ break;
149000
149226
  }
149001
- return link2?.model;
149002
- }
149003
- async sendPermCardToThread(message, sessionID) {
149004
- const link2 = await this.deps.sessionMap.resolveBySession(sessionID);
149005
- const card = buildPermCard({ ...link2?.perm ? { current: link2.perm } : {}, sid: sessionID });
149006
- await this.deps.sender.replyCard(message.messageId, card);
149007
- }
149008
- async patchCard(messageId, card) {
149009
- if (!messageId) return;
149010
- const res = await this.deps.sender.patchCard(messageId, card);
149011
- if (!res.ok) this.deps.log.warn("\u5411\u5BFC\u5361\u7247\u66F4\u65B0\u5931\u8D25", { error: res.error ?? "unknown" });
149012
- }
149013
- async loadModels() {
149014
- try {
149015
- return [...await this.deps.listModels()];
149016
- } catch (err2) {
149017
- this.deps.log.warn("\u6A21\u578B\u5217\u8868\u83B7\u53D6\u5931\u8D25", { error: errorMessage(err2) });
149018
- return [];
149019
- }
149020
- }
149021
- recentModelsLimit() {
149022
- return this.deps.recentModelsLimit ?? 5;
149023
- }
149024
- /**
149025
- * 扫描允许根目录的一级子目录(表单目录下拉选项来源)。
149026
- * 任何失败静默降级为空列表(只保留「手动输入」与根目录两项),绝不抛异常。
149027
- */
149028
- async scanRoot(root) {
149029
- if (!root) return [];
149030
- const scan = this.deps.scanRootSubdirs ?? scanRootSubdirs;
149031
- try {
149032
- return [...await scan(root)];
149033
- } catch (err2) {
149034
- this.deps.log.warn("\u6839\u76EE\u5F55\u5B50\u76EE\u5F55\u626B\u63CF\u5931\u8D25", { error: errorMessage(err2) });
149035
- return [];
149036
- }
149037
- }
149038
- async threadSessionID(message) {
149039
- if (!message.threadId) return void 0;
149040
- const link2 = await this.deps.sessionMap.resolveByThread(message.threadId);
149041
- return link2?.sessionID;
149042
- }
149043
- /** 文本回执:话题内引用触发消息(留在话题),主聊天流直接发送。 */
149044
- async reply(message, text) {
149045
- if (message.threadId) {
149046
- await this.deps.sender.replyText(message.messageId, text);
149047
- return;
149227
+ case "session.idle": {
149228
+ const data = event.data;
149229
+ deps.markEnded(data.sessionID);
149230
+ deps.applyRun(data.sessionID, { type: "execution.succeeded" });
149231
+ break;
149048
149232
  }
149049
- await this.deps.sender.sendText(message.chatId, text);
149050
149233
  }
149051
- };
149052
- function confirmInput(state) {
149053
- return {
149054
- ...state.title ? { title: state.title } : {},
149055
- ...state.dir ? { dir: state.dir } : {},
149056
- ...state.model ? { model: state.model } : {},
149057
- ...state.perm ? { perm: state.perm } : {}
149058
- };
149059
- }
149060
- function permUsageText() {
149061
- const list = PERMISSION_PRESETS.map((p) => `\`${p.id}\`\uFF08${presetInfo(p.id).icon}${p.label}\uFF09`).join("\u3001");
149062
- return `\u672A\u77E5\u6743\u9650\u6863\u4F4D\u3002\u53EF\u7528\uFF1A${list}\u3002
149063
- \u4F8B\u5982\uFF1A\`/perm edit\`\u3002`;
149064
149234
  }
149065
- function modelSwitchReply(outcome, requested) {
149066
- const effective = outcome?.effective ?? requested;
149067
- const ref = `**${modelLabel(effective)}**
149068
- \`${effective.providerID}/${effective.id}\``;
149069
- if (outcome?.mismatch) {
149070
- return [
149071
- `\u26A0\uFE0F \u6A21\u578B\u53EF\u80FD\u672A\u751F\u6548\uFF1A\u8BF7\u6C42 **${modelLabel(outcome.requested)}**\uFF0C\u670D\u52A1\u7AEF\u5B9E\u9645\u4E3A ${ref}\u3002`,
149072
- "\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\uFF1B\u5386\u53F2\u6D88\u606F\u4ECD\u662F\u65E7\u6A21\u578B\uFF0C\u5C5E\u6B63\u5E38\u3002"
149073
- ].join("\n");
149074
- }
149075
- if (outcome && !outcome.verified) {
149076
- return `\u2705 \u5DF2\u8BF7\u6C42\u5207\u6362\u6A21\u578B\uFF1A${ref}
149077
- \u672A\u80FD\u8BFB\u56DE\u6821\u9A8C${outcome.warning ? `\uFF08${outcome.warning}\uFF09` : ""}\uFF1B\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\u3002`;
149235
+ function extractErrorText(error) {
149236
+ if (!error) return "unknown";
149237
+ if (typeof error === "string") return error.slice(0, 300);
149238
+ if (Array.isArray(error)) return contentToText(error).slice(0, 300) || "unknown";
149239
+ if (typeof error === "object") {
149240
+ const record2 = error;
149241
+ if (typeof record2.message === "string") return record2.message.slice(0, 300);
149242
+ if (typeof record2.type === "string") return record2.type;
149078
149243
  }
149079
- return `\u2705 \u5DF2\u5207\u6362\u6A21\u578B\uFF1A${ref}
149080
- \u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\uFF1B\u8BE5\u4F1A\u8BDD\u6B64\u524D\u7684\u5386\u53F2\u6D88\u606F\u4ECD\u663E\u793A\u65E7\u6A21\u578B\uFF0C\u5C5E\u6B63\u5E38\u3002`;
149244
+ return "unknown";
149081
149245
  }
149082
- function modelSwitchDoneCard(outcome, requested) {
149083
- const effective = outcome?.effective ?? requested;
149084
- if (outcome?.mismatch) {
149085
- return buildSetupDoneCard("\u26A0\uFE0F \u6A21\u578B\u53EF\u80FD\u672A\u751F\u6548", [
149086
- `\u8BF7\u6C42\uFF1A**${modelLabel(outcome.requested)}**`,
149087
- `\u5B9E\u9645\uFF1A**${modelLabel(effective)}**`,
149088
- "\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\uFF1B\u5386\u53F2\u6D88\u606F\u4ECD\u662F\u65E7\u6A21\u578B\uFF0C\u5C5E\u6B63\u5E38\u3002"
149089
- ]);
149246
+ function contentToText(content) {
149247
+ if (typeof content === "string") return content;
149248
+ if (Array.isArray(content)) {
149249
+ return content.map((item) => {
149250
+ if (typeof item === "string") return item;
149251
+ if (item && typeof item === "object" && typeof item.text === "string") {
149252
+ return item.text;
149253
+ }
149254
+ return "";
149255
+ }).filter(Boolean).join("\n");
149090
149256
  }
149091
- if (outcome && !outcome.verified) {
149092
- return buildSetupDoneCard("\u2705 \u5DF2\u5207\u6362\u6A21\u578B", [
149093
- `\u5F53\u524D\u6A21\u578B\uFF1A**${modelLabel(effective)}**`,
149094
- "\uFF08\u672A\u80FD\u8BFB\u56DE\u6821\u9A8C\uFF1B\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\u3002\uFF09"
149095
- ]);
149257
+ if (content && typeof content === "object" && typeof content.text === "string") {
149258
+ return content.text;
149096
149259
  }
149097
- return buildSetupDoneCard("\u2705 \u5DF2\u5207\u6362\u6A21\u578B", [
149098
- `\u5F53\u524D\u6A21\u578B\uFF1A**${modelLabel(effective)}**`,
149099
- "\uFF08\u5207\u6362\u53EA\u5F71\u54CD**\u540E\u7EED**\u56DE\u590D\uFF1B\u5386\u53F2\u6D88\u606F\u4ECD\u662F\u65E7\u6A21\u578B\uFF0C\u5C5E\u6B63\u5E38\u3002\uFF09"
149100
- ]);
149260
+ return "";
149101
149261
  }
149102
- function setupToast(value3) {
149103
- switch (value3.kind) {
149104
- case "dir":
149105
- return toast3("success", "\u5DF2\u9009\u62E9\u76EE\u5F55");
149106
- case "model":
149107
- return toast3("success", "\u5DF2\u9009\u62E9\u6A21\u578B");
149108
- case "perm":
149109
- return toast3("success", "\u5DF2\u9009\u62E9\u6743\u9650");
149110
- case "more":
149111
- return toast3("info", "\u5DF2\u7FFB\u9875");
149112
- case "confirm":
149113
- return toast3("success", "\u6B63\u5728\u521B\u5EFA\u4F1A\u8BDD\u2026");
149114
- case "form":
149115
- return toast3("info", "\u5DF2\u6253\u5F00\u8868\u5355");
149116
- case "cancel":
149117
- default:
149118
- return toast3("info", "\u5DF2\u53D6\u6D88");
149262
+
149263
+ // src/runtime/card-action-router.ts
149264
+ function routeCardAction(action, deps) {
149265
+ const formResponse = deps.handleForm(action);
149266
+ if (formResponse) return formResponse;
149267
+ const value3 = action.rawValue;
149268
+ if (parseStopActionValue(value3)) {
149269
+ return deps.handleStop(action);
149270
+ }
149271
+ const hasForm = action.formValue !== void 0;
149272
+ const routed = hasForm || isSetupFormAction(value3) || Boolean(parseSessionCardValue(value3)) || Boolean(parseSetupCardValue(value3));
149273
+ deps.log.debug("\u5361\u7247\u56DE\u8C03\u8DEF\u7531", {
149274
+ hasForm,
149275
+ hasValue: value3 !== void 0,
149276
+ valueKeys: value3 && typeof value3 === "object" ? Object.keys(value3) : [],
149277
+ routedTo: routed ? "commands" : "approvals"
149278
+ });
149279
+ if (routed) {
149280
+ return deps.handleCommands(action);
149119
149281
  }
149120
- }
149121
- function toast3(type, content) {
149122
- return { toast: { type, content } };
149123
- }
149124
- function clampPageSize(value3) {
149125
- const base2 = typeof value3 === "number" && Number.isFinite(value3) ? Math.floor(value3) : 8;
149126
- return Math.min(20, Math.max(5, base2));
149282
+ return deps.handleApprovals(action);
149127
149283
  }
149128
149284
 
149129
149285
  // src/index.ts
@@ -149528,26 +149684,13 @@ async function start(ctx, config, log2, logSink) {
149528
149684
  log: log2,
149529
149685
  logLevel: config.logLevel,
149530
149686
  onMessage: (message) => handleMessage(message),
149531
- onCardAction: async (action) => {
149532
- const formResponse = formRelay.handleCardAction(action);
149533
- if (formResponse) return formResponse;
149534
- const value3 = action.rawValue;
149535
- if (parseStopActionValue(value3)) {
149536
- return stop.handleCardAction(action);
149537
- }
149538
- const hasForm = action.formValue !== void 0;
149539
- const routed = hasForm || isSetupFormAction(value3) || Boolean(parseSessionCardValue(value3)) || Boolean(parseSetupCardValue(value3));
149540
- log2.debug("\u5361\u7247\u56DE\u8C03\u8DEF\u7531", {
149541
- hasForm,
149542
- hasValue: value3 !== void 0,
149543
- valueKeys: value3 && typeof value3 === "object" ? Object.keys(value3) : [],
149544
- routedTo: routed ? "commands" : "approvals"
149545
- });
149546
- if (routed) {
149547
- return commands.handleCardAction(action);
149548
- }
149549
- return approvals.handleCardAction(action);
149550
- }
149687
+ onCardAction: (action) => routeCardAction(action, {
149688
+ log: log2,
149689
+ handleForm: (a) => formRelay.handleCardAction(a),
149690
+ handleStop: (a) => stop.handleCardAction(a),
149691
+ handleCommands: (a) => commands.handleCardAction(a),
149692
+ handleApprovals: (a) => approvals.handleCardAction(a)
149693
+ })
149551
149694
  });
149552
149695
  const abort = new AbortController();
149553
149696
  const subscription = (async () => {
@@ -149559,127 +149702,21 @@ async function start(ctx, config, log2, logSink) {
149559
149702
  if (!abort.signal.aborted) log2.error("\u4E8B\u4EF6\u8BA2\u9605\u5F02\u5E38\u9000\u51FA", { error: errorMessage(err2) });
149560
149703
  }
149561
149704
  })();
149705
+ const eventDeps = {
149706
+ log: log2,
149707
+ touch: (sessionID) => executions.touch(sessionID),
149708
+ markStarted: (sessionID) => executions.markStarted(sessionID),
149709
+ markEnded: (sessionID) => executions.markEnded(sessionID),
149710
+ applyRun: (sessionID, event) => runs.apply(sessionID, event),
149711
+ onPermissionAsked: (data) => approvals.onAsked(data),
149712
+ onPermissionReplied: (data) => approvals.onReplied(data),
149713
+ onFormCreated: (data) => formRelay.onCreated(data),
149714
+ onFormReplied: (data) => formRelay.onReplied(data),
149715
+ onFormCancelled: (data) => formRelay.onCancelled(data),
149716
+ notifyFailure: (sessionID, error) => notifyFailure(sessionID, error)
149717
+ };
149562
149718
  async function handleEvent(event) {
149563
- const touched = event.data?.sessionID;
149564
- if (typeof touched === "string") executions.touch(touched);
149565
- switch (event.type) {
149566
- case "permission.asked":
149567
- void approvals.onAsked(event.data).catch((err2) => log2.warn("\u5904\u7406 permission.asked \u5931\u8D25", { error: errorMessage(err2) }));
149568
- break;
149569
- case "permission.replied":
149570
- approvals.onReplied(event.data);
149571
- break;
149572
- case "form.created":
149573
- void formRelay.onCreated(event.data).catch((err2) => log2.warn("\u5904\u7406 form.created \u5931\u8D25", { error: errorMessage(err2) }));
149574
- break;
149575
- case "form.replied":
149576
- formRelay.onReplied(event.data);
149577
- break;
149578
- case "form.cancelled":
149579
- formRelay.onCancelled(event.data);
149580
- break;
149581
- case "session.text.started": {
149582
- const data = event.data;
149583
- runs.apply(data.sessionID, {
149584
- type: "text.started",
149585
- ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
149586
- });
149587
- break;
149588
- }
149589
- case "session.text.delta": {
149590
- const data = event.data;
149591
- runs.apply(data.sessionID, {
149592
- type: "text.delta",
149593
- delta: data.delta,
149594
- ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
149595
- });
149596
- break;
149597
- }
149598
- case "session.text.ended": {
149599
- const data = event.data;
149600
- runs.apply(data.sessionID, {
149601
- type: "text.ended",
149602
- ...data.text ? { text: data.text } : {},
149603
- ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
149604
- });
149605
- break;
149606
- }
149607
- case "session.tool.input.started": {
149608
- const data = event.data;
149609
- runs.apply(data.sessionID, {
149610
- type: "tool.input.started",
149611
- id: data.id,
149612
- name: data.name,
149613
- ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
149614
- });
149615
- break;
149616
- }
149617
- case "session.tool.input.ended": {
149618
- const data = event.data;
149619
- runs.apply(data.sessionID, { type: "tool.input.ended", id: data.id, input: data.input });
149620
- break;
149621
- }
149622
- case "session.tool.success": {
149623
- const data = event.data;
149624
- runs.apply(data.sessionID, {
149625
- type: "tool.success",
149626
- id: data.id,
149627
- output: contentToText(data.content)
149628
- });
149629
- break;
149630
- }
149631
- case "session.tool.error": {
149632
- const data = event.data;
149633
- runs.apply(data.sessionID, {
149634
- type: "tool.error",
149635
- id: data.id,
149636
- output: extractErrorText(data.error ?? data.content)
149637
- });
149638
- break;
149639
- }
149640
- case "session.execution.started": {
149641
- const data = event.data;
149642
- executions.markStarted(data.sessionID);
149643
- runs.apply(data.sessionID, { type: "execution.started" });
149644
- break;
149645
- }
149646
- case "session.execution.succeeded": {
149647
- const data = event.data;
149648
- executions.markEnded(data.sessionID);
149649
- runs.apply(data.sessionID, { type: "execution.succeeded" });
149650
- break;
149651
- }
149652
- case "session.execution.failed": {
149653
- const data = event.data;
149654
- executions.markEnded(data.sessionID);
149655
- runs.apply(data.sessionID, { type: "execution.failed", error: extractErrorText(data.error) });
149656
- void notifyFailure(data.sessionID, data.error);
149657
- break;
149658
- }
149659
- case "session.execution.interrupted": {
149660
- const data = event.data;
149661
- executions.markEnded(data.sessionID);
149662
- runs.apply(data.sessionID, { type: "execution.failed", error: `\u5DF2\u4E2D\u65AD\uFF08${data.reason ?? "unknown"}\uFF09` });
149663
- break;
149664
- }
149665
- case "session.status": {
149666
- const data = event.data;
149667
- const statusType = data.status?.type;
149668
- if (statusType === "idle") {
149669
- executions.markEnded(data.sessionID);
149670
- runs.apply(data.sessionID, { type: "execution.succeeded" });
149671
- } else if (statusType === "busy" || statusType === "retry") {
149672
- executions.markStarted(data.sessionID);
149673
- }
149674
- break;
149675
- }
149676
- case "session.idle": {
149677
- const data = event.data;
149678
- executions.markEnded(data.sessionID);
149679
- runs.apply(data.sessionID, { type: "execution.succeeded" });
149680
- break;
149681
- }
149682
- }
149719
+ await routeEvent(event, eventDeps);
149683
149720
  }
149684
149721
  async function notifyFailure(sessionID, error) {
149685
149722
  const link2 = await sessionMap.resolveBySession(sessionID);
@@ -149828,33 +149865,6 @@ async function replyPermission(ctx, input) {
149828
149865
  throw err2;
149829
149866
  }
149830
149867
  }
149831
- function extractErrorText(error) {
149832
- if (!error) return "unknown";
149833
- if (typeof error === "string") return error.slice(0, 300);
149834
- if (Array.isArray(error)) return contentToText(error).slice(0, 300) || "unknown";
149835
- if (typeof error === "object") {
149836
- const record2 = error;
149837
- if (typeof record2.message === "string") return record2.message.slice(0, 300);
149838
- if (typeof record2.type === "string") return record2.type;
149839
- }
149840
- return "unknown";
149841
- }
149842
- function contentToText(content) {
149843
- if (typeof content === "string") return content;
149844
- if (Array.isArray(content)) {
149845
- return content.map((item) => {
149846
- if (typeof item === "string") return item;
149847
- if (item && typeof item === "object" && typeof item.text === "string") {
149848
- return item.text;
149849
- }
149850
- return "";
149851
- }).filter(Boolean).join("\n");
149852
- }
149853
- if (content && typeof content === "object" && typeof content.text === "string") {
149854
- return content.text;
149855
- }
149856
- return "";
149857
- }
149858
149868
  async function promptSession(ctx, sessionID, text, delivery) {
149859
149869
  const api = ctx.session.prompt;
149860
149870
  await api({ sessionID, text, delivery });