@turmind/halo 1.0.2 → 1.0.3
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/212-c5f046048be9c367.js +1 -0
- package/admin-out/_next/static/chunks/4-e7ba0b1ced30190b.js +1 -0
- package/admin-out/_next/static/chunks/app/page-5a6cf2b73a12b849.js +1 -0
- package/admin-out/_next/static/css/62b8bf49e6404ffc.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +2 -1
- package/bundled-docs/design/cron.md +9 -3
- package/bundled-docs/design/session.md +3 -1
- package/bundled-docs/design/ws.md +21 -4
- package/bundled-docs/dev/api.md +37 -3
- package/bundled-docs/dev/env.md +2 -0
- package/bundled-docs/dev/tools.md +13 -4
- package/bundled-docs/guide/chat.md +1 -1
- package/bundled-docs/guide/workspace.md +2 -2
- package/bundled-docs/requirements/settings.md +8 -0
- package/dist/index.js +416 -247
- package/package.json +1 -1
- package/templates/agents/__apply_agent__/agent.yaml +1 -1
- package/templates/agents/__evo_agent__/agent.yaml +1 -1
- package/templates/agents/__score__/agent.yaml +1 -1
- package/templates/agents/deep-executor/agent.yaml +1 -1
- package/templates/agents/default/agent.yaml +1 -1
- package/templates/agents/goal/agent.yaml +1 -1
- package/templates/skills/agent/SKILL.md +2 -2
- package/admin-out/_next/static/chunks/212-e217e00ce6cc2bdc.js +0 -1
- package/admin-out/_next/static/chunks/397-fc5a406d80d46ca7.js +0 -1
- package/admin-out/_next/static/chunks/app/page-f8208e0c7f315faa.js +0 -1
- package/admin-out/_next/static/css/80eca65cb72e5c69.css +0 -3
- /package/admin-out/_next/static/{WvYu_osMZVpohYN4ntkpS → CWrUwTypkebkhwZ5i82_G}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{WvYu_osMZVpohYN4ntkpS → CWrUwTypkebkhwZ5i82_G}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -479,6 +479,17 @@ function modelsRegistry() {
|
|
|
479
479
|
_modelsRegistryCache = loadModelsRegistry();
|
|
480
480
|
return _modelsRegistryCache;
|
|
481
481
|
}
|
|
482
|
+
function getSystemConfig() {
|
|
483
|
+
let mtimeMs = 0;
|
|
484
|
+
try {
|
|
485
|
+
mtimeMs = fs3.statSync(SYSTEM_CONFIG_PATH).mtimeMs;
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
if (!_systemConfigCache || _systemConfigCache.mtimeMs !== mtimeMs) {
|
|
489
|
+
_systemConfigCache = { mtimeMs, data: loadYamlFile(SYSTEM_CONFIG_PATH) };
|
|
490
|
+
}
|
|
491
|
+
return _systemConfigCache.data;
|
|
492
|
+
}
|
|
482
493
|
function getSettings() {
|
|
483
494
|
let mtimeMs = 0;
|
|
484
495
|
try {
|
|
@@ -514,7 +525,7 @@ function settingsValue(dotPath) {
|
|
|
514
525
|
return v == null ? void 0 : typeof v === "string" ? v : String(v);
|
|
515
526
|
}
|
|
516
527
|
function configValue(dotPath) {
|
|
517
|
-
return readYamlValue(
|
|
528
|
+
return readYamlValue(getSystemConfig(), dotPath);
|
|
518
529
|
}
|
|
519
530
|
function envInt(key, fallback) {
|
|
520
531
|
const v = process.env[key];
|
|
@@ -711,7 +722,7 @@ function modelSupportsAudio(modelId, override) {
|
|
|
711
722
|
function getModelsRegistry() {
|
|
712
723
|
return modelsRegistry();
|
|
713
724
|
}
|
|
714
|
-
var HALO_HOME2, HALO_GLOBAL_DIR, HALO_SECRETS_DIR, _modelsRegistryCache,
|
|
725
|
+
var HALO_HOME2, HALO_GLOBAL_DIR, HALO_SECRETS_DIR, _modelsRegistryCache, SYSTEM_CONFIG_PATH, _systemConfigCache, SETTINGS_PATH, _settingsCache, _schemaDefaultsCache, ENV_PATTERN, _wsSettingsCache, config, HIDDEN_DIRS_DEFAULT, HIDDEN_FILES_DEFAULT;
|
|
715
726
|
var init_config = __esm({
|
|
716
727
|
"packages/server/dist/config.js"() {
|
|
717
728
|
"use strict";
|
|
@@ -720,7 +731,8 @@ var init_config = __esm({
|
|
|
720
731
|
HALO_GLOBAL_DIR = path3.join(HALO_HOME2, "global");
|
|
721
732
|
HALO_SECRETS_DIR = path3.join(HALO_HOME2, "secrets");
|
|
722
733
|
_modelsRegistryCache = null;
|
|
723
|
-
|
|
734
|
+
SYSTEM_CONFIG_PATH = path3.join(HALO_SECRETS_DIR, "config.yaml");
|
|
735
|
+
_systemConfigCache = null;
|
|
724
736
|
SETTINGS_PATH = path3.join(HALO_SECRETS_DIR, "settings.yaml");
|
|
725
737
|
_settingsCache = null;
|
|
726
738
|
_schemaDefaultsCache = null;
|
|
@@ -2206,7 +2218,7 @@ var init_init = __esm({
|
|
|
2206
2218
|
path6.resolve(__dirname, "..", "bundled-docs"),
|
|
2207
2219
|
path6.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
2208
2220
|
];
|
|
2209
|
-
TEMPLATE_VERSION =
|
|
2221
|
+
TEMPLATE_VERSION = 49;
|
|
2210
2222
|
VERSION_FILE = ".template-version";
|
|
2211
2223
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
2212
2224
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -6935,6 +6947,16 @@ function buildBwrapArgs(opts) {
|
|
|
6935
6947
|
args.push("--bind", dir, dir);
|
|
6936
6948
|
}
|
|
6937
6949
|
}
|
|
6950
|
+
for (const rel of WORKSPACE_HIDDEN_DIRS) {
|
|
6951
|
+
const dir = path15.join(opts.workspaceRoot, rel);
|
|
6952
|
+
if (existsSync(dir))
|
|
6953
|
+
args.push("--tmpfs", dir);
|
|
6954
|
+
}
|
|
6955
|
+
for (const rel of WORKSPACE_HIDDEN_FILES) {
|
|
6956
|
+
const file = path15.join(opts.workspaceRoot, rel);
|
|
6957
|
+
if (existsSync(file))
|
|
6958
|
+
args.push("--ro-bind", "/dev/null", file);
|
|
6959
|
+
}
|
|
6938
6960
|
args.push("--proc", "/proc");
|
|
6939
6961
|
args.push("--dev", "/dev");
|
|
6940
6962
|
args.push("--clearenv");
|
|
@@ -6969,6 +6991,9 @@ function assertPathAllowed(filePath, opts, write = false) {
|
|
|
6969
6991
|
const resolved = realpathBounded(filePath);
|
|
6970
6992
|
const wsRoot = realpathBounded(opts.workspaceRoot);
|
|
6971
6993
|
if (resolved === wsRoot || resolved.startsWith(wsRoot + "/")) {
|
|
6994
|
+
if (isHiddenWorkspacePath(resolved, wsRoot)) {
|
|
6995
|
+
throw new Error(`Access denied: "${filePath}" is outside the allowed sandbox paths`);
|
|
6996
|
+
}
|
|
6972
6997
|
if (write && opts.accessLevel === "readonly") {
|
|
6973
6998
|
throw new Error(`Access denied: readonly session cannot write to "${filePath}"`);
|
|
6974
6999
|
}
|
|
@@ -6991,6 +7016,18 @@ function isHiddenGlobalPath(resolved) {
|
|
|
6991
7016
|
}
|
|
6992
7017
|
return false;
|
|
6993
7018
|
}
|
|
7019
|
+
function isHiddenWorkspacePath(resolved, wsRoot) {
|
|
7020
|
+
for (const rel of WORKSPACE_HIDDEN_FILES) {
|
|
7021
|
+
if (resolved === path15.join(wsRoot, rel))
|
|
7022
|
+
return true;
|
|
7023
|
+
}
|
|
7024
|
+
for (const rel of WORKSPACE_HIDDEN_DIRS) {
|
|
7025
|
+
const dir = path15.join(wsRoot, rel);
|
|
7026
|
+
if (resolved === dir || resolved.startsWith(dir + "/"))
|
|
7027
|
+
return true;
|
|
7028
|
+
}
|
|
7029
|
+
return false;
|
|
7030
|
+
}
|
|
6994
7031
|
function buildExecScriptArgs(workspaceRoot, command) {
|
|
6995
7032
|
return ["bash", "-c", `cd "$1" && shift && ${command}`, "bash", workspaceRoot];
|
|
6996
7033
|
}
|
|
@@ -7194,7 +7231,7 @@ async function sandboxReaddir(dirPath, opts) {
|
|
|
7194
7231
|
return { name: isDir ? name.slice(0, -1) : name, isDirectory: isDir };
|
|
7195
7232
|
});
|
|
7196
7233
|
}
|
|
7197
|
-
var execAsync, execFileAsync, HOME, HALO_HOME3, GLOBAL_DIR, KILL_GRACE_MS, _bwrapAvailable, DEFAULT_HIDDEN_DIRS, DEFAULT_HIDDEN_FILES, _hiddenDirs, _hiddenFiles, _writableDirs;
|
|
7234
|
+
var execAsync, execFileAsync, HOME, HALO_HOME3, GLOBAL_DIR, KILL_GRACE_MS, _bwrapAvailable, DEFAULT_HIDDEN_DIRS, DEFAULT_HIDDEN_FILES, WORKSPACE_HIDDEN_DIRS, WORKSPACE_HIDDEN_FILES, _hiddenDirs, _hiddenFiles, _writableDirs;
|
|
7198
7235
|
var init_sandbox = __esm({
|
|
7199
7236
|
"packages/server/dist/tools/sandbox.js"() {
|
|
7200
7237
|
"use strict";
|
|
@@ -7237,6 +7274,8 @@ var init_sandbox = __esm({
|
|
|
7237
7274
|
"~/.halo/global/cron.db-wal",
|
|
7238
7275
|
"~/.halo/global/cron.db-shm"
|
|
7239
7276
|
];
|
|
7277
|
+
WORKSPACE_HIDDEN_DIRS = [".halo/sessions", ".halo/logs", ".halo/evo"];
|
|
7278
|
+
WORKSPACE_HIDDEN_FILES = [".halo/halo.db", ".halo/halo.db-wal", ".halo/halo.db-shm"];
|
|
7240
7279
|
_hiddenDirs = DEFAULT_HIDDEN_DIRS;
|
|
7241
7280
|
_hiddenFiles = DEFAULT_HIDDEN_FILES;
|
|
7242
7281
|
_writableDirs = [];
|
|
@@ -8704,8 +8743,15 @@ var init_i18n = __esm({
|
|
|
8704
8743
|
"cmd.unknown": { zh: "\u672A\u77E5\u547D\u4EE4: {cmd}\n\u53D1 /help \u67E5\u770B\u53EF\u7528\u547D\u4EE4", en: "Unknown command: {cmd}\nSend /help to see available commands" },
|
|
8705
8744
|
// ── Channel handler messages ──
|
|
8706
8745
|
"handler.workspace_missing": { zh: "\u26A0\uFE0F \u8FD9\u4E2A bot \u7ED1\u5B9A\u7684 workspace \u5DF2\u4E0D\u5B58\u5728\uFF1A\n{path}\n\n\u8BF7\u5230 web \u7AEF\u7684 Channels \u8BBE\u7F6E\u91CC\u66F4\u65B0\u7ED1\u5B9A\u76EE\u5F55\u3002", en: "\u26A0\uFE0F The workspace bound to this bot no longer exists:\n{path}\n\nPlease update the binding in web Channels settings." },
|
|
8707
|
-
|
|
8746
|
+
// Both hints must read as "received, will be answered" — the message IS
|
|
8747
|
+
// queued (sendUserMessage queues compacting/busy sessions). Telling the user
|
|
8748
|
+
// to re-send would make them duplicate a message that was never lost.
|
|
8749
|
+
"handler.compacting": { zh: "\u23F3 \u6B63\u5728\u6574\u7406\u4E0A\u4E0B\u6587\uFF0C\u6D88\u606F\u5DF2\u6536\u5230\uFF0C\u6574\u7406\u5B8C\u6210\u540E\u7ACB\u5373\u5904\u7406\uFF08\u901A\u5E38 30 \u79D2\u5185\uFF09", en: "\u23F3 Compacting context \u2014 got your message, will process as soon as it finishes (usually under 30s)" },
|
|
8708
8750
|
"handler.queued": { zh: "\u{1F504} \u521A\u624D\u90A3\u6761\u8FD8\u5728\u5904\u7406\u4E2D\uFF0C\u6D88\u606F\u5DF2\u6392\u961F\uFF0C\u8BF7\u7A0D\u5019", en: "\u{1F504} Previous message still processing, queued" },
|
|
8751
|
+
// Outbound media upload failed — tells the user why no attachment showed up.
|
|
8752
|
+
// Plain text (no backticks): slack/feishu render markdown differently, and the
|
|
8753
|
+
// filename is already delimited by the surrounding punctuation.
|
|
8754
|
+
"handler.upload_failed": { zh: "\u26A0\uFE0F \u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF1A{name} \u2014 {error}", en: "\u26A0\uFE0F Failed to upload {name}: {error}" },
|
|
8709
8755
|
"handler.workspace_gone": { zh: "\u26A0\uFE0F workspace \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5230 web \u7AEF\u66F4\u65B0\u7ED1\u5B9A", en: "\u26A0\uFE0F Workspace does not exist, please update binding in web" },
|
|
8710
8756
|
"handler.not_allowed": { zh: "\u26A0\uFE0F \u4F60\u4E0D\u5728\u8FD9\u4E2A bot \u7684\u5141\u8BB8\u5217\u8868\u4E2D", en: "\u26A0\uFE0F You are not in this bot's allowed list" },
|
|
8711
8757
|
"handler.start_greeting": { zh: "\u{1F44B} Halo bot \u5DF2\u5C31\u7EEA\u3002\u76F4\u63A5\u53D1\u6D88\u606F\u5F00\u59CB\u5BF9\u8BDD\u3002\u53D1 /help \u67E5\u770B\u53EF\u7528\u547D\u4EE4\u3002", en: "\u{1F44B} Halo bot ready. Send a message to start. /help for commands." },
|
|
@@ -10471,9 +10517,11 @@ ${cleaned}` : cleaned;
|
|
|
10471
10517
|
const overThreshold = session.lastContextTokens > session.contextConfig.maxTokens * session.contextConfig.compressAt;
|
|
10472
10518
|
if (!overThreshold)
|
|
10473
10519
|
return;
|
|
10520
|
+
if (this.compactCut(session.agent.messages) === 0)
|
|
10521
|
+
return;
|
|
10522
|
+
const taskId = session.parentId ? session.id : void 0;
|
|
10474
10523
|
try {
|
|
10475
10524
|
session.isCompacting = true;
|
|
10476
|
-
const taskId = session.parentId ? session.id : void 0;
|
|
10477
10525
|
const preflightText = `Compacting context (${Math.round(session.lastContextTokens / 1e3)}K tokens)\u2026`;
|
|
10478
10526
|
this.emitEvent(session.id, { type: "system", text: preflightText, taskId });
|
|
10479
10527
|
const result = await this.selfCompactSession(session.id);
|
|
@@ -10484,9 +10532,12 @@ ${cleaned}` : cleaned;
|
|
|
10484
10532
|
${result.summary}`, agentName: "user", report: true, taskId });
|
|
10485
10533
|
this.emitEvent(session.id, { type: "compacted", totalTokens: result.estimatedTokens, taskId });
|
|
10486
10534
|
console.debug(`[SessionManager] Auto-compact ${session.id}: ${result.olderCount} messages compacted`);
|
|
10535
|
+
} else {
|
|
10536
|
+
this.emitEvent(session.id, { type: "system", text: "Compaction skipped \u2014 no summary produced", taskId });
|
|
10487
10537
|
}
|
|
10488
10538
|
} catch (err) {
|
|
10489
10539
|
console.debug(`[SessionManager] Auto-compact ${session.id} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
10540
|
+
this.emitEvent(session.id, { type: "system", text: "Compaction failed \u2014 context unchanged", taskId });
|
|
10490
10541
|
} finally {
|
|
10491
10542
|
session.isCompacting = false;
|
|
10492
10543
|
}
|
|
@@ -10578,19 +10629,25 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
10578
10629
|
}
|
|
10579
10630
|
}
|
|
10580
10631
|
/**
|
|
10581
|
-
*
|
|
10582
|
-
*
|
|
10583
|
-
*
|
|
10632
|
+
* Compute the compaction cut point: messages[0..cut) get summarized,
|
|
10633
|
+
* messages[cut..] are kept. Returns 0 when there is nothing to compact
|
|
10634
|
+
* (too few messages) — the "compaction is feasible" predicate.
|
|
10635
|
+
*
|
|
10636
|
+
* Single source of truth shared by selfCompactSession AND the
|
|
10637
|
+
* pre-preflight feasibility gates in maybeAutoCompact / compactSession:
|
|
10638
|
+
* gate and actual compact must run the same computation on the same
|
|
10639
|
+
* `session.agent.messages` reference or they drift ("gate said yes,
|
|
10640
|
+
* compact said no" re-creates the orphan-preflight bug).
|
|
10641
|
+
*
|
|
10642
|
+
* Note the tail loop only ever moves `cut` UP (a tail of user tool_result
|
|
10643
|
+
* messages extends the summarized region — they're protocol continuations
|
|
10644
|
+
* of an assistant turn, not standalone turns), so cut === 0 is exactly
|
|
10645
|
+
* the `messages.length <= keepCount` case.
|
|
10584
10646
|
*/
|
|
10585
|
-
|
|
10586
|
-
const session = this.sessions.get(sessionId);
|
|
10587
|
-
if (!session)
|
|
10588
|
-
return null;
|
|
10589
|
-
const messages2 = session.agent.messages;
|
|
10647
|
+
compactCut(messages2) {
|
|
10590
10648
|
const keepCount = config.compact.keep_messages;
|
|
10591
10649
|
if (messages2.length <= keepCount)
|
|
10592
|
-
return
|
|
10593
|
-
session.compactedThisTurn = true;
|
|
10650
|
+
return 0;
|
|
10594
10651
|
let cut = Math.max(0, messages2.length - keepCount);
|
|
10595
10652
|
while (cut < messages2.length) {
|
|
10596
10653
|
const m = messages2[cut];
|
|
@@ -10601,8 +10658,22 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
10601
10658
|
}
|
|
10602
10659
|
break;
|
|
10603
10660
|
}
|
|
10661
|
+
return cut;
|
|
10662
|
+
}
|
|
10663
|
+
/**
|
|
10664
|
+
* Self-compact: let the current agent summarize its own conversation.
|
|
10665
|
+
* The agent already has full context cached, so this is fast and lossless.
|
|
10666
|
+
* Returns the summary text, or null if compaction was not needed/failed.
|
|
10667
|
+
*/
|
|
10668
|
+
async selfCompactSession(sessionId, signal) {
|
|
10669
|
+
const session = this.sessions.get(sessionId);
|
|
10670
|
+
if (!session)
|
|
10671
|
+
return null;
|
|
10672
|
+
const messages2 = session.agent.messages;
|
|
10673
|
+
const cut = this.compactCut(messages2);
|
|
10604
10674
|
if (cut === 0)
|
|
10605
10675
|
return null;
|
|
10676
|
+
session.compactedThisTurn = true;
|
|
10606
10677
|
const olderCount = cut;
|
|
10607
10678
|
const compactInstruction = [
|
|
10608
10679
|
"Summarize the conversation so far into a concise summary that preserves:",
|
|
@@ -10686,9 +10757,8 @@ ${summaryText}` }]
|
|
|
10686
10757
|
return "running";
|
|
10687
10758
|
if (session.isCompacting)
|
|
10688
10759
|
return "already";
|
|
10689
|
-
const keepCount = config.compact.keep_messages;
|
|
10690
10760
|
const rawCount = session.agent.messages.length;
|
|
10691
|
-
if (
|
|
10761
|
+
if (this.compactCut(session.agent.messages) === 0)
|
|
10692
10762
|
return "nothing";
|
|
10693
10763
|
const ac = await this.beginCompact(sessionId);
|
|
10694
10764
|
if (!ac)
|
|
@@ -10700,8 +10770,10 @@ ${summaryText}` }]
|
|
|
10700
10770
|
this.emitEvent(sessionId, { type: "system", text: preflightText, taskId });
|
|
10701
10771
|
try {
|
|
10702
10772
|
const result = await this.selfCompactSession(sessionId, ac.signal);
|
|
10703
|
-
if (!result)
|
|
10773
|
+
if (!result) {
|
|
10774
|
+
this.emitEvent(sessionId, { type: "system", text: "Compaction skipped \u2014 no summary produced", taskId });
|
|
10704
10775
|
return "nothing";
|
|
10776
|
+
}
|
|
10705
10777
|
session.lastContextTokens = result.estimatedTokens;
|
|
10706
10778
|
const noticeText = `Context compacted: ${result.olderCount} older messages summarized`;
|
|
10707
10779
|
this.emitEvent(sessionId, { type: "system", text: noticeText, taskId });
|
|
@@ -10717,6 +10789,7 @@ ${result.summary}`, agentName: "user", report: true, taskId });
|
|
|
10717
10789
|
this.emitEvent(sessionId, { type: "system", text: "Compact cancelled" });
|
|
10718
10790
|
return "nothing";
|
|
10719
10791
|
}
|
|
10792
|
+
this.emitEvent(sessionId, { type: "system", text: "Compaction failed \u2014 context unchanged", taskId });
|
|
10720
10793
|
throw err;
|
|
10721
10794
|
} finally {
|
|
10722
10795
|
this.endCompact(sessionId);
|
|
@@ -13574,8 +13647,8 @@ function applyPathPick(value, ref, pick) {
|
|
|
13574
13647
|
const before = value.slice(0, markerStart);
|
|
13575
13648
|
const kind = ref.kind === "scope" ? "scope" : ref.kind === "image" ? "image" : pick.isImage ? "image" : "file";
|
|
13576
13649
|
const needsQuote = pick.insertPath.includes(" ");
|
|
13577
|
-
const
|
|
13578
|
-
return `${before}@${kind} ${
|
|
13650
|
+
const path63 = pick.isDir ? needsQuote ? `"${pick.insertPath}/` : `${pick.insertPath}/` : needsQuote ? `"${pick.insertPath}" ` : `${pick.insertPath} `;
|
|
13651
|
+
return `${before}@${kind} ${path63}`;
|
|
13579
13652
|
}
|
|
13580
13653
|
var IMAGE_EXTS2;
|
|
13581
13654
|
var init_path_suggest_engine = __esm({
|
|
@@ -15095,12 +15168,12 @@ None yet.
|
|
|
15095
15168
|
}
|
|
15096
15169
|
console.log(`[Workspace] Initialized workspace: ${this.projectRoot}`);
|
|
15097
15170
|
}
|
|
15098
|
-
async readFile(
|
|
15099
|
-
const fullPath = this.validatePath(
|
|
15171
|
+
async readFile(path63) {
|
|
15172
|
+
const fullPath = this.validatePath(path63);
|
|
15100
15173
|
return readFile(fullPath, "utf-8");
|
|
15101
15174
|
}
|
|
15102
|
-
async writeFile(
|
|
15103
|
-
const fullPath = this.validatePath(
|
|
15175
|
+
async writeFile(path63, content) {
|
|
15176
|
+
const fullPath = this.validatePath(path63);
|
|
15104
15177
|
const dir = resolve(fullPath, "..");
|
|
15105
15178
|
await mkdir(dir, { recursive: true });
|
|
15106
15179
|
await writeFile(fullPath, content, "utf-8");
|
|
@@ -15120,26 +15193,26 @@ None yet.
|
|
|
15120
15193
|
}
|
|
15121
15194
|
return results;
|
|
15122
15195
|
}
|
|
15123
|
-
async fileExists(
|
|
15196
|
+
async fileExists(path63) {
|
|
15124
15197
|
try {
|
|
15125
|
-
const fullPath = this.validatePath(
|
|
15198
|
+
const fullPath = this.validatePath(path63);
|
|
15126
15199
|
await access(fullPath);
|
|
15127
15200
|
return true;
|
|
15128
15201
|
} catch {
|
|
15129
15202
|
return false;
|
|
15130
15203
|
}
|
|
15131
15204
|
}
|
|
15132
|
-
validatePath(
|
|
15133
|
-
const fullPath = isAbsolute(
|
|
15205
|
+
validatePath(path63) {
|
|
15206
|
+
const fullPath = isAbsolute(path63) ? resolve(path63) : resolve(this.projectRoot, path63);
|
|
15134
15207
|
const root = resolve(this.projectRoot);
|
|
15135
15208
|
if (fullPath !== root && !fullPath.startsWith(root + sep)) {
|
|
15136
|
-
throw new WorkspaceError(`Path "${
|
|
15209
|
+
throw new WorkspaceError(`Path "${path63}" is outside the workspace directory "${this.projectRoot}"`);
|
|
15137
15210
|
}
|
|
15138
15211
|
try {
|
|
15139
15212
|
const real = realpathSync2(fullPath);
|
|
15140
15213
|
const realRoot = realpathSync2(root);
|
|
15141
15214
|
if (real !== realRoot && !real.startsWith(realRoot + sep)) {
|
|
15142
|
-
throw new WorkspaceError(`Path "${
|
|
15215
|
+
throw new WorkspaceError(`Path "${path63}" is outside the workspace directory "${this.projectRoot}"`);
|
|
15143
15216
|
}
|
|
15144
15217
|
} catch (err) {
|
|
15145
15218
|
if (err instanceof WorkspaceError)
|
|
@@ -15252,10 +15325,10 @@ var init_git_manager = __esm({
|
|
|
15252
15325
|
throw new GitManagerError(`Failed to commit: ${message2}`);
|
|
15253
15326
|
}
|
|
15254
15327
|
}
|
|
15255
|
-
async getDiff(
|
|
15328
|
+
async getDiff(path63) {
|
|
15256
15329
|
try {
|
|
15257
|
-
if (
|
|
15258
|
-
return await this.git.diff(["HEAD", "--",
|
|
15330
|
+
if (path63) {
|
|
15331
|
+
return await this.git.diff(["HEAD", "--", path63]);
|
|
15259
15332
|
}
|
|
15260
15333
|
return await this.git.diff(["HEAD"]);
|
|
15261
15334
|
} catch {
|
|
@@ -15344,12 +15417,12 @@ var init_git_manager = __esm({
|
|
|
15344
15417
|
const code = parts[0];
|
|
15345
15418
|
const status = code[0];
|
|
15346
15419
|
const isRenameOrCopy = status === "R" || status === "C";
|
|
15347
|
-
const
|
|
15348
|
-
if (!
|
|
15420
|
+
const path63 = isRenameOrCopy ? parts[2] : parts[1];
|
|
15421
|
+
if (!path63 || seen.has(path63))
|
|
15349
15422
|
continue;
|
|
15350
|
-
seen.add(
|
|
15423
|
+
seen.add(path63);
|
|
15351
15424
|
files.push({
|
|
15352
|
-
path:
|
|
15425
|
+
path: path63,
|
|
15353
15426
|
status,
|
|
15354
15427
|
from: isRenameOrCopy ? parts[1] : void 0
|
|
15355
15428
|
});
|
|
@@ -15497,9 +15570,9 @@ var init_git_manager = __esm({
|
|
|
15497
15570
|
* empty for an added file or the root commit) and `modified` is the version
|
|
15498
15571
|
* at `commit` (empty for a deletion). The working-tree/staged path above is
|
|
15499
15572
|
* left entirely untouched (commit === undefined preserves existing behavior). */
|
|
15500
|
-
async getFileDiff(
|
|
15573
|
+
async getFileDiff(path63, staged, from, commit) {
|
|
15501
15574
|
if (commit) {
|
|
15502
|
-
const parentPath = from ||
|
|
15575
|
+
const parentPath = from || path63;
|
|
15503
15576
|
let original2 = "";
|
|
15504
15577
|
try {
|
|
15505
15578
|
original2 = await this.git.show([`${commit}^:${parentPath}`]);
|
|
@@ -15508,13 +15581,13 @@ var init_git_manager = __esm({
|
|
|
15508
15581
|
}
|
|
15509
15582
|
let modified2 = "";
|
|
15510
15583
|
try {
|
|
15511
|
-
modified2 = await this.git.show([`${commit}:${
|
|
15584
|
+
modified2 = await this.git.show([`${commit}:${path63}`]);
|
|
15512
15585
|
} catch {
|
|
15513
15586
|
modified2 = "";
|
|
15514
15587
|
}
|
|
15515
15588
|
return { original: original2, modified: modified2 };
|
|
15516
15589
|
}
|
|
15517
|
-
const headPath = from ||
|
|
15590
|
+
const headPath = from || path63;
|
|
15518
15591
|
let original = "";
|
|
15519
15592
|
try {
|
|
15520
15593
|
original = await this.git.show([`HEAD:${headPath}`]);
|
|
@@ -15524,13 +15597,13 @@ var init_git_manager = __esm({
|
|
|
15524
15597
|
let modified = "";
|
|
15525
15598
|
if (staged) {
|
|
15526
15599
|
try {
|
|
15527
|
-
modified = await this.git.show([`:${
|
|
15600
|
+
modified = await this.git.show([`:${path63}`]);
|
|
15528
15601
|
} catch {
|
|
15529
15602
|
modified = "";
|
|
15530
15603
|
}
|
|
15531
15604
|
} else {
|
|
15532
15605
|
try {
|
|
15533
|
-
modified = await this.workspace.readFile(
|
|
15606
|
+
modified = await this.workspace.readFile(path63);
|
|
15534
15607
|
} catch {
|
|
15535
15608
|
modified = "";
|
|
15536
15609
|
}
|
|
@@ -15561,6 +15634,24 @@ function isInTempDir(filePath) {
|
|
|
15561
15634
|
const tmp = tempDir();
|
|
15562
15635
|
return resolved === tmp || resolved.startsWith(tmp + path32.sep);
|
|
15563
15636
|
}
|
|
15637
|
+
function extractMediaPaths(text5) {
|
|
15638
|
+
const mediaPaths = [];
|
|
15639
|
+
const stripped = text5.replace(MEDIA_MARKER_RE, (_m, p) => {
|
|
15640
|
+
if (p)
|
|
15641
|
+
mediaPaths.push(p);
|
|
15642
|
+
return "";
|
|
15643
|
+
});
|
|
15644
|
+
return { text: stripped, mediaPaths };
|
|
15645
|
+
}
|
|
15646
|
+
function extractMediaMessage(text5) {
|
|
15647
|
+
const { text: stripped, mediaPaths } = extractMediaPaths(text5);
|
|
15648
|
+
return { text: stripped.replace(/\n{3,}/g, "\n\n").trim(), mediaPaths };
|
|
15649
|
+
}
|
|
15650
|
+
function isMediaPathAllowed(filePath, workspacePath) {
|
|
15651
|
+
const resolved = path32.resolve(filePath);
|
|
15652
|
+
const ws = path32.resolve(workspacePath);
|
|
15653
|
+
return resolved === ws || resolved.startsWith(ws + path32.sep) || isInTempDir(resolved);
|
|
15654
|
+
}
|
|
15564
15655
|
function classifyMedia(filePath) {
|
|
15565
15656
|
const ext = path32.extname(filePath).toLowerCase();
|
|
15566
15657
|
if (IMAGE_EXTS3.has(ext))
|
|
@@ -15571,10 +15662,11 @@ function classifyMedia(filePath) {
|
|
|
15571
15662
|
return "voice";
|
|
15572
15663
|
return "other";
|
|
15573
15664
|
}
|
|
15574
|
-
var IMAGE_EXTS3, VIDEO_EXTS, VOICE_EXTS;
|
|
15665
|
+
var MEDIA_MARKER_RE, IMAGE_EXTS3, VIDEO_EXTS, VOICE_EXTS;
|
|
15575
15666
|
var init_media = __esm({
|
|
15576
15667
|
"packages/server/dist/channels/shared/media.js"() {
|
|
15577
15668
|
"use strict";
|
|
15669
|
+
MEDIA_MARKER_RE = /^MEDIA:\s*(\S.*?)\s*$/gm;
|
|
15578
15670
|
IMAGE_EXTS3 = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]);
|
|
15579
15671
|
VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v", ".webm", ".avi"]);
|
|
15580
15672
|
VOICE_EXTS = /* @__PURE__ */ new Set([".ogg", ".oga", ".opus"]);
|
|
@@ -16486,12 +16578,20 @@ function createGitRoutes() {
|
|
|
16486
16578
|
const git = new GitManager(new Workspace(projectPath));
|
|
16487
16579
|
return { git, projectPath };
|
|
16488
16580
|
}
|
|
16581
|
+
async function getGitForRead(projectId) {
|
|
16582
|
+
const res = await getGit(projectId);
|
|
16583
|
+
if ("error" in res)
|
|
16584
|
+
return res;
|
|
16585
|
+
if (!await res.git.isRepoRoot())
|
|
16586
|
+
return { notRepo: true };
|
|
16587
|
+
return res;
|
|
16588
|
+
}
|
|
16489
16589
|
app2.get("/git/status", async (c) => {
|
|
16490
16590
|
try {
|
|
16491
|
-
const res = await
|
|
16591
|
+
const res = await getGitForRead(c.req.query("projectId"));
|
|
16492
16592
|
if ("error" in res)
|
|
16493
16593
|
return c.json({ error: res.error }, res.status);
|
|
16494
|
-
if (
|
|
16594
|
+
if ("notRepo" in res)
|
|
16495
16595
|
return c.json({ isRepo: false });
|
|
16496
16596
|
const status = await res.git.getStatus();
|
|
16497
16597
|
return c.json({ isRepo: true, ...status });
|
|
@@ -16503,9 +16603,11 @@ function createGitRoutes() {
|
|
|
16503
16603
|
});
|
|
16504
16604
|
app2.get("/git/ignored", async (c) => {
|
|
16505
16605
|
try {
|
|
16506
|
-
const res = await
|
|
16606
|
+
const res = await getGitForRead(c.req.query("projectId"));
|
|
16507
16607
|
if ("error" in res)
|
|
16508
16608
|
return c.json({ error: res.error }, res.status);
|
|
16609
|
+
if ("notRepo" in res)
|
|
16610
|
+
return c.json({ isRepo: false, ignored: [] });
|
|
16509
16611
|
const ignored = await res.git.getIgnoredPaths();
|
|
16510
16612
|
return c.json({ ignored });
|
|
16511
16613
|
} catch (err) {
|
|
@@ -16520,11 +16622,13 @@ function createGitRoutes() {
|
|
|
16520
16622
|
const staged = c.req.query("staged") === "1";
|
|
16521
16623
|
const from = c.req.query("from") || void 0;
|
|
16522
16624
|
const commit = c.req.query("commit") || void 0;
|
|
16523
|
-
const res = await
|
|
16625
|
+
const res = await getGitForRead(c.req.query("projectId"));
|
|
16524
16626
|
if ("error" in res)
|
|
16525
16627
|
return c.json({ error: res.error }, res.status);
|
|
16526
16628
|
if (!filePath)
|
|
16527
16629
|
return c.json({ error: "path is required" }, 400);
|
|
16630
|
+
if ("notRepo" in res)
|
|
16631
|
+
return c.json({ path: filePath, original: "", modified: "" });
|
|
16528
16632
|
if (!validatePath(filePath, res.projectPath)) {
|
|
16529
16633
|
return c.json({ error: "Path traversal not allowed" }, 403);
|
|
16530
16634
|
}
|
|
@@ -16538,9 +16642,11 @@ function createGitRoutes() {
|
|
|
16538
16642
|
});
|
|
16539
16643
|
app2.get("/git/log", async (c) => {
|
|
16540
16644
|
try {
|
|
16541
|
-
const res = await
|
|
16645
|
+
const res = await getGitForRead(c.req.query("projectId"));
|
|
16542
16646
|
if ("error" in res)
|
|
16543
16647
|
return c.json({ error: res.error }, res.status);
|
|
16648
|
+
if ("notRepo" in res)
|
|
16649
|
+
return c.json({ commits: [] });
|
|
16544
16650
|
const limitRaw = Number(c.req.query("limit"));
|
|
16545
16651
|
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, 2e3) : 50;
|
|
16546
16652
|
const commits = await res.git.getLog(limit);
|
|
@@ -16554,11 +16660,13 @@ function createGitRoutes() {
|
|
|
16554
16660
|
app2.get("/git/commit-files", async (c) => {
|
|
16555
16661
|
try {
|
|
16556
16662
|
const hash = c.req.query("hash");
|
|
16557
|
-
const res = await
|
|
16663
|
+
const res = await getGitForRead(c.req.query("projectId"));
|
|
16558
16664
|
if ("error" in res)
|
|
16559
16665
|
return c.json({ error: res.error }, res.status);
|
|
16560
16666
|
if (!hash)
|
|
16561
16667
|
return c.json({ error: "hash is required" }, 400);
|
|
16668
|
+
if ("notRepo" in res)
|
|
16669
|
+
return c.json({ files: [] });
|
|
16562
16670
|
const files = await res.git.getCommitFiles(hash);
|
|
16563
16671
|
return c.json({ files });
|
|
16564
16672
|
} catch (err) {
|
|
@@ -16667,9 +16775,11 @@ function createGitRoutes() {
|
|
|
16667
16775
|
});
|
|
16668
16776
|
app2.get("/git/remotes", async (c) => {
|
|
16669
16777
|
try {
|
|
16670
|
-
const res = await
|
|
16778
|
+
const res = await getGitForRead(c.req.query("projectId"));
|
|
16671
16779
|
if ("error" in res)
|
|
16672
16780
|
return c.json({ error: res.error }, res.status);
|
|
16781
|
+
if ("notRepo" in res)
|
|
16782
|
+
return c.json({ remotes: [] });
|
|
16673
16783
|
const remotes = await res.git.getRemotes();
|
|
16674
16784
|
return c.json({ remotes });
|
|
16675
16785
|
} catch (err) {
|
|
@@ -16792,9 +16902,11 @@ function createGitRoutes() {
|
|
|
16792
16902
|
});
|
|
16793
16903
|
app2.get("/git/remote/protocol", async (c) => {
|
|
16794
16904
|
try {
|
|
16795
|
-
const res = await
|
|
16905
|
+
const res = await getGitForRead(c.req.query("projectId"));
|
|
16796
16906
|
if ("error" in res)
|
|
16797
16907
|
return c.json({ error: res.error }, res.status);
|
|
16908
|
+
if ("notRepo" in res)
|
|
16909
|
+
return c.json({ url: "", protocol: "other" });
|
|
16798
16910
|
return c.json(getRemoteProtocol(res.projectPath));
|
|
16799
16911
|
} catch (err) {
|
|
16800
16912
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -17657,8 +17769,8 @@ function detectOrphans(tree, declared) {
|
|
|
17657
17769
|
if (!block || typeof block !== "object")
|
|
17658
17770
|
continue;
|
|
17659
17771
|
for (const k of Object.keys(block)) {
|
|
17660
|
-
const
|
|
17661
|
-
if (!declaredMap.has(
|
|
17772
|
+
const path63 = `${namespaceId}.${kindWord}.${k}`;
|
|
17773
|
+
if (!declaredMap.has(path63)) {
|
|
17662
17774
|
out.push({ namespaceId, kind: kindWord === "params" ? "param" : "secret", key: k });
|
|
17663
17775
|
}
|
|
17664
17776
|
}
|
|
@@ -20459,6 +20571,17 @@ function setupWebSocketHandler(deps) {
|
|
|
20459
20571
|
});
|
|
20460
20572
|
};
|
|
20461
20573
|
}
|
|
20574
|
+
function reclaimIfAbandoned(client) {
|
|
20575
|
+
if (!client.unsubscribeEvents)
|
|
20576
|
+
return;
|
|
20577
|
+
const closed = client.ws.readyState === client.ws.CLOSED;
|
|
20578
|
+
if (!closed && Date.now() - client.lastClientPingAt <= CLIENT_SILENCE_LIMIT_MS)
|
|
20579
|
+
return;
|
|
20580
|
+
client.unsubscribeEvents();
|
|
20581
|
+
client.unsubscribeEvents = null;
|
|
20582
|
+
console.debug(`[WS] Released listener for ${closed ? "closed" : "abandoned"} connection session=${client.sessionId}`);
|
|
20583
|
+
sendJson(client.ws, { type: "listener:released", sessionId: client.sessionId });
|
|
20584
|
+
}
|
|
20462
20585
|
wss2.on("connection", (ws) => {
|
|
20463
20586
|
const fileWatcher = new WorkspaceWatcher();
|
|
20464
20587
|
const gitDirWatcher = new GitDirWatcher();
|
|
@@ -20473,7 +20596,8 @@ function setupWebSocketHandler(deps) {
|
|
|
20473
20596
|
gitDirWatcher,
|
|
20474
20597
|
terminalManager,
|
|
20475
20598
|
backgroundSaves: /* @__PURE__ */ new Map(),
|
|
20476
|
-
unsubscribeEvents: null
|
|
20599
|
+
unsubscribeEvents: null,
|
|
20600
|
+
lastClientPingAt: Date.now()
|
|
20477
20601
|
};
|
|
20478
20602
|
fileWatcher.setCallback((evt) => {
|
|
20479
20603
|
sendJson(ws, { type: "file:changed", path: evt.path, action: evt.action });
|
|
@@ -20488,6 +20612,7 @@ function setupWebSocketHandler(deps) {
|
|
|
20488
20612
|
missedPongs = 0;
|
|
20489
20613
|
});
|
|
20490
20614
|
const keepaliveTimer = setInterval(() => {
|
|
20615
|
+
reclaimIfAbandoned(client);
|
|
20491
20616
|
if (missedPongs >= 2) {
|
|
20492
20617
|
ws.terminate();
|
|
20493
20618
|
return;
|
|
@@ -20501,6 +20626,7 @@ function setupWebSocketHandler(deps) {
|
|
|
20501
20626
|
sendJson(ws, { type: "state:snapshot", snapshot: { activePlan: null, agents: [], recentMessages: [], maxContextTokens: config.model.maxContextTokens } });
|
|
20502
20627
|
let messageQueue = Promise.resolve();
|
|
20503
20628
|
ws.on("message", (raw) => {
|
|
20629
|
+
client.lastClientPingAt = Date.now();
|
|
20504
20630
|
let msg;
|
|
20505
20631
|
try {
|
|
20506
20632
|
const text5 = typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
@@ -20678,7 +20804,7 @@ function setupWebSocketHandler(deps) {
|
|
|
20678
20804
|
client2.unsubscribeEvents = sm.registerEventListener(client2.sessionId, createEventListener(client2));
|
|
20679
20805
|
const ctxConfig = await sm.getContextConfig(client2.sessionId);
|
|
20680
20806
|
sendJson(ws, { type: "state:snapshot", snapshot: { activePlan: null, agents: [], recentMessages: [], sessionId: client2.sessionId, maxContextTokens: ctxConfig.maxTokens, agentId } });
|
|
20681
|
-
} else if (client2.sessionId !== msg.sessionId) {
|
|
20807
|
+
} else if (client2.sessionId !== msg.sessionId || !client2.unsubscribeEvents) {
|
|
20682
20808
|
client2.unsubscribeEvents?.();
|
|
20683
20809
|
client2.sessionId = msg.sessionId;
|
|
20684
20810
|
client2.unsubscribeEvents = sm.registerEventListener(client2.sessionId, createEventListener(client2));
|
|
@@ -20802,7 +20928,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
20802
20928
|
if (client2.sessionId && client2.sessionId !== msg.sessionId) {
|
|
20803
20929
|
saveSession(client2);
|
|
20804
20930
|
}
|
|
20805
|
-
if (client2.sessionId
|
|
20931
|
+
if (client2.sessionId) {
|
|
20806
20932
|
client2.unsubscribeEvents?.();
|
|
20807
20933
|
client2.unsubscribeEvents = null;
|
|
20808
20934
|
client2.sessionId = null;
|
|
@@ -20911,7 +21037,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
20911
21037
|
});
|
|
20912
21038
|
console.log("[WS] WebSocket handler initialized");
|
|
20913
21039
|
}
|
|
20914
|
-
var detachedSessions;
|
|
21040
|
+
var CLIENT_SILENCE_LIMIT_MS, detachedSessions;
|
|
20915
21041
|
var init_handler = __esm({
|
|
20916
21042
|
"packages/server/dist/ws/handler.js"() {
|
|
20917
21043
|
"use strict";
|
|
@@ -20924,6 +21050,7 @@ var init_handler = __esm({
|
|
|
20924
21050
|
init_terminal_manager();
|
|
20925
21051
|
init_commands2();
|
|
20926
21052
|
init_goal_mode();
|
|
21053
|
+
CLIENT_SILENCE_LIMIT_MS = 3 * 6e4;
|
|
20927
21054
|
detachedSessions = /* @__PURE__ */ new Map();
|
|
20928
21055
|
}
|
|
20929
21056
|
});
|
|
@@ -21078,7 +21205,9 @@ function listAllCronTargets() {
|
|
|
21078
21205
|
}
|
|
21079
21206
|
return out;
|
|
21080
21207
|
}
|
|
21081
|
-
async function dispatchToTargets(
|
|
21208
|
+
async function dispatchToTargets(rawText, targets, workspacePath) {
|
|
21209
|
+
const { text: strippedText, mediaPaths } = extractMediaMessage(rawText);
|
|
21210
|
+
const media = mediaPaths.length > 0 ? { paths: mediaPaths, workspacePath } : void 0;
|
|
21082
21211
|
const out = [];
|
|
21083
21212
|
for (const t2 of targets) {
|
|
21084
21213
|
const handler = _registry.get(t2.channelType);
|
|
@@ -21093,7 +21222,7 @@ async function dispatchToTargets(text5, targets) {
|
|
|
21093
21222
|
continue;
|
|
21094
21223
|
}
|
|
21095
21224
|
try {
|
|
21096
|
-
const results = await handler.dispatch(t2.accountId,
|
|
21225
|
+
const results = handler.supportsMedia ? await handler.dispatch(t2.accountId, strippedText, t2.chatId, media) : await handler.dispatch(t2.accountId, rawText, t2.chatId);
|
|
21097
21226
|
for (const r of results)
|
|
21098
21227
|
out.push(r);
|
|
21099
21228
|
} catch (err) {
|
|
@@ -21112,6 +21241,7 @@ var _registry;
|
|
|
21112
21241
|
var init_dispatcher = __esm({
|
|
21113
21242
|
"packages/server/dist/cron/dispatcher.js"() {
|
|
21114
21243
|
"use strict";
|
|
21244
|
+
init_media();
|
|
21115
21245
|
_registry = /* @__PURE__ */ new Map();
|
|
21116
21246
|
}
|
|
21117
21247
|
});
|
|
@@ -21542,7 +21672,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
21542
21672
|
}
|
|
21543
21673
|
let dispatchResults = [];
|
|
21544
21674
|
if (status === "succeeded" && targets.length > 0) {
|
|
21545
|
-
dispatchResults = await dispatchToTargets(trimmedOut, targets);
|
|
21675
|
+
dispatchResults = await dispatchToTargets(trimmedOut, targets, job.workspacePath);
|
|
21546
21676
|
if (dispatchResults.length > 0 && dispatchResults.every((r) => !r.ok)) {
|
|
21547
21677
|
status = "failed";
|
|
21548
21678
|
failureReason = `all ${dispatchResults.length} target(s) failed: ${dispatchResults.map((r) => `${r.channelType}:${r.error ?? "?"}`).join("; ")}`;
|
|
@@ -22277,12 +22407,12 @@ var init_accounts3 = __esm({
|
|
|
22277
22407
|
});
|
|
22278
22408
|
|
|
22279
22409
|
// packages/server/dist/channels/telegram/event-adapter.js
|
|
22280
|
-
var HARD_CHARS,
|
|
22410
|
+
var HARD_CHARS, TelegramResponder;
|
|
22281
22411
|
var init_event_adapter = __esm({
|
|
22282
22412
|
"packages/server/dist/channels/telegram/event-adapter.js"() {
|
|
22283
22413
|
"use strict";
|
|
22414
|
+
init_media();
|
|
22284
22415
|
HARD_CHARS = 4e3;
|
|
22285
|
-
MEDIA_MARKER_RE = /^MEDIA:\s*(\S.*?)\s*$/gm;
|
|
22286
22416
|
TelegramResponder = class {
|
|
22287
22417
|
buffer = "";
|
|
22288
22418
|
deps;
|
|
@@ -22355,12 +22485,7 @@ var init_event_adapter = __esm({
|
|
|
22355
22485
|
return limit;
|
|
22356
22486
|
}
|
|
22357
22487
|
async dispatchChunk(chunk) {
|
|
22358
|
-
const mediaPaths =
|
|
22359
|
-
const text5 = chunk.replace(MEDIA_MARKER_RE, (_m, p) => {
|
|
22360
|
-
if (p)
|
|
22361
|
-
mediaPaths.push(p.trim());
|
|
22362
|
-
return "";
|
|
22363
|
-
}).replace(/\n{3,}/g, "\n\n").trim();
|
|
22488
|
+
const { text: text5, mediaPaths } = extractMediaMessage(chunk);
|
|
22364
22489
|
if (text5) {
|
|
22365
22490
|
try {
|
|
22366
22491
|
await this.deps.sendText(text5);
|
|
@@ -22380,6 +22505,21 @@ var init_event_adapter = __esm({
|
|
|
22380
22505
|
}
|
|
22381
22506
|
});
|
|
22382
22507
|
|
|
22508
|
+
// packages/server/dist/channels/shared/busy-hint.js
|
|
22509
|
+
function busyHint(sm, sessionId, lang) {
|
|
22510
|
+
if (sm.isSessionCompacting(sessionId))
|
|
22511
|
+
return t("handler.compacting", lang);
|
|
22512
|
+
if (sm.isSessionRunning(sessionId))
|
|
22513
|
+
return t("handler.queued", lang);
|
|
22514
|
+
return null;
|
|
22515
|
+
}
|
|
22516
|
+
var init_busy_hint = __esm({
|
|
22517
|
+
"packages/server/dist/channels/shared/busy-hint.js"() {
|
|
22518
|
+
"use strict";
|
|
22519
|
+
init_i18n();
|
|
22520
|
+
}
|
|
22521
|
+
});
|
|
22522
|
+
|
|
22383
22523
|
// packages/server/dist/channels/shared/session-prefix.js
|
|
22384
22524
|
function sessionPrefix(channel, userId) {
|
|
22385
22525
|
return `${channel}_${userId}_`;
|
|
@@ -22392,7 +22532,6 @@ var init_session_prefix = __esm({
|
|
|
22392
22532
|
|
|
22393
22533
|
// packages/server/dist/channels/telegram/handler.js
|
|
22394
22534
|
import { Bot, InputFile } from "grammy";
|
|
22395
|
-
import path52 from "node:path";
|
|
22396
22535
|
function inferMediaKind(filePath) {
|
|
22397
22536
|
const cls = classifyMedia(filePath);
|
|
22398
22537
|
if (cls === "image")
|
|
@@ -22668,23 +22807,16 @@ async function handleUserMessage(args) {
|
|
|
22668
22807
|
const sm = registry2.getOrCreate(workspace);
|
|
22669
22808
|
const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
|
|
22670
22809
|
const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateActiveSession(sm, userId, activeOverrides2, accessLevel, workspace));
|
|
22671
|
-
|
|
22672
|
-
|
|
22673
|
-
|
|
22674
|
-
}
|
|
22675
|
-
if (sm.isSessionRunning(sessionId)) {
|
|
22676
|
-
await ctx.reply(t("handler.queued", lang));
|
|
22677
|
-
}
|
|
22810
|
+
const hint = busyHint(sm, sessionId, lang);
|
|
22811
|
+
if (hint)
|
|
22812
|
+
await ctx.reply(hint);
|
|
22678
22813
|
if (!unsubscribers.has(sessionId)) {
|
|
22679
22814
|
const responder = new TelegramResponder({
|
|
22680
22815
|
sendText: async (chunk) => {
|
|
22681
22816
|
await bot.api.sendMessage(chatId, chunk, { parse_mode: void 0 });
|
|
22682
22817
|
},
|
|
22683
22818
|
sendMedia: async (filePath) => {
|
|
22684
|
-
|
|
22685
|
-
const ws = path52.resolve(workspace);
|
|
22686
|
-
const inWorkspace = resolved === ws || resolved.startsWith(ws + path52.sep);
|
|
22687
|
-
if (!inWorkspace && !isInTempDir(resolved)) {
|
|
22819
|
+
if (!isMediaPathAllowed(filePath, workspace)) {
|
|
22688
22820
|
console.log(`[telegram] sendMedia blocked: ${filePath} not under workspace`);
|
|
22689
22821
|
return;
|
|
22690
22822
|
}
|
|
@@ -22742,6 +22874,7 @@ var init_handler2 = __esm({
|
|
|
22742
22874
|
init_commands2();
|
|
22743
22875
|
init_goal_mode();
|
|
22744
22876
|
init_i18n();
|
|
22877
|
+
init_busy_hint();
|
|
22745
22878
|
init_commands();
|
|
22746
22879
|
init_media();
|
|
22747
22880
|
init_session_prefix();
|
|
@@ -22750,7 +22883,7 @@ var init_handler2 = __esm({
|
|
|
22750
22883
|
|
|
22751
22884
|
// packages/server/dist/routes/telegram.js
|
|
22752
22885
|
import { Hono as Hono13 } from "hono";
|
|
22753
|
-
import
|
|
22886
|
+
import path52 from "node:path";
|
|
22754
22887
|
import { Bot as Bot2 } from "grammy";
|
|
22755
22888
|
import fs47 from "node:fs";
|
|
22756
22889
|
function createTelegramRoutes(deps) {
|
|
@@ -22780,7 +22913,7 @@ function createTelegramRoutes(deps) {
|
|
|
22780
22913
|
return c.json({ error: "botToken required" }, 400);
|
|
22781
22914
|
if (!body.workspacePath)
|
|
22782
22915
|
return c.json({ error: "workspacePath required" }, 400);
|
|
22783
|
-
if (!
|
|
22916
|
+
if (!path52.isAbsolute(body.workspacePath))
|
|
22784
22917
|
return c.json({ error: "workspacePath must be absolute" }, 400);
|
|
22785
22918
|
if (!fs47.existsSync(body.workspacePath))
|
|
22786
22919
|
return c.json({ error: "workspace path not found" }, 400);
|
|
@@ -23269,12 +23402,12 @@ var init_accounts4 = __esm({
|
|
|
23269
23402
|
});
|
|
23270
23403
|
|
|
23271
23404
|
// packages/server/dist/channels/wechat/event-adapter.js
|
|
23272
|
-
var HARD_CHARS2,
|
|
23405
|
+
var HARD_CHARS2, WeixinResponder;
|
|
23273
23406
|
var init_event_adapter2 = __esm({
|
|
23274
23407
|
"packages/server/dist/channels/wechat/event-adapter.js"() {
|
|
23275
23408
|
"use strict";
|
|
23409
|
+
init_media();
|
|
23276
23410
|
HARD_CHARS2 = 3500;
|
|
23277
|
-
MEDIA_MARKER_RE2 = /^MEDIA:\s*(\S.*?)\s*$/gm;
|
|
23278
23411
|
WeixinResponder = class {
|
|
23279
23412
|
buffer = "";
|
|
23280
23413
|
deps;
|
|
@@ -23295,13 +23428,13 @@ var init_event_adapter2 = __esm({
|
|
|
23295
23428
|
case "error":
|
|
23296
23429
|
if (event.error) {
|
|
23297
23430
|
this.flushAll();
|
|
23298
|
-
void this.dispatchChunk(
|
|
23431
|
+
void this.dispatchChunk(`\u274C ${event.error}`);
|
|
23299
23432
|
}
|
|
23300
23433
|
break;
|
|
23301
23434
|
case "system":
|
|
23302
23435
|
if (event.text) {
|
|
23303
23436
|
this.flushAll();
|
|
23304
|
-
void this.dispatchChunk(
|
|
23437
|
+
void this.dispatchChunk(`\u2139\uFE0F ${event.text}`);
|
|
23305
23438
|
}
|
|
23306
23439
|
break;
|
|
23307
23440
|
case "complete":
|
|
@@ -23355,12 +23488,7 @@ var init_event_adapter2 = __esm({
|
|
|
23355
23488
|
* goes out as a WeChat message (if non-empty after trim).
|
|
23356
23489
|
*/
|
|
23357
23490
|
async dispatchChunk(chunk) {
|
|
23358
|
-
const mediaPaths =
|
|
23359
|
-
const text5 = chunk.replace(MEDIA_MARKER_RE2, (_m, p) => {
|
|
23360
|
-
if (p)
|
|
23361
|
-
mediaPaths.push(p.trim());
|
|
23362
|
-
return "";
|
|
23363
|
-
}).replace(/\n{3,}/g, "\n\n").trim();
|
|
23491
|
+
const { text: text5, mediaPaths } = extractMediaMessage(chunk);
|
|
23364
23492
|
if (text5) {
|
|
23365
23493
|
try {
|
|
23366
23494
|
await this.deps.sendText(text5);
|
|
@@ -23488,7 +23616,7 @@ var init_cdn = __esm({
|
|
|
23488
23616
|
|
|
23489
23617
|
// packages/server/dist/channels/wechat/send-media.js
|
|
23490
23618
|
import fs48 from "node:fs/promises";
|
|
23491
|
-
import
|
|
23619
|
+
import path53 from "node:path";
|
|
23492
23620
|
function inferKind(filePath) {
|
|
23493
23621
|
const cls = classifyMedia(filePath);
|
|
23494
23622
|
if (cls === "image")
|
|
@@ -23538,7 +23666,7 @@ async function sendMediaFile(params) {
|
|
|
23538
23666
|
aesKeyBase64,
|
|
23539
23667
|
fileSizeCiphertext: prep.filesize,
|
|
23540
23668
|
fileSize: prep.rawsize,
|
|
23541
|
-
fileName:
|
|
23669
|
+
fileName: path53.basename(filePath)
|
|
23542
23670
|
});
|
|
23543
23671
|
const clientId = `halo-media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
23544
23672
|
const body = {
|
|
@@ -23702,12 +23830,8 @@ var init_login = __esm({
|
|
|
23702
23830
|
|
|
23703
23831
|
// packages/server/dist/channels/wechat/handler.js
|
|
23704
23832
|
import fs49 from "node:fs";
|
|
23705
|
-
import
|
|
23833
|
+
import path54 from "node:path";
|
|
23706
23834
|
import QRCode from "qrcode";
|
|
23707
|
-
function isPathAllowed(filePath, workspacePath) {
|
|
23708
|
-
const resolved = path55.resolve(filePath);
|
|
23709
|
-
return resolved.startsWith(path55.resolve(workspacePath)) || isInTempDir(resolved);
|
|
23710
|
-
}
|
|
23711
23835
|
function startWeixinChannel(deps) {
|
|
23712
23836
|
const { registry: registry2, db } = deps;
|
|
23713
23837
|
const runners = /* @__PURE__ */ new Map();
|
|
@@ -23991,21 +24115,13 @@ async function handleInbound(args) {
|
|
|
23991
24115
|
const sm = registry2.getOrCreate(account.workspacePath);
|
|
23992
24116
|
const sessionAccessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
|
|
23993
24117
|
const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateActiveSession2(sm, fromUserId, activeOverrides2, sessionAccessLevel, account.workspacePath));
|
|
23994
|
-
|
|
24118
|
+
const hint = busyHint(sm, sessionId, lang);
|
|
24119
|
+
if (hint) {
|
|
23995
24120
|
await sendToUser({
|
|
23996
24121
|
account,
|
|
23997
24122
|
toUserId: fromUserId,
|
|
23998
24123
|
contextToken: msg.context_token,
|
|
23999
|
-
text:
|
|
24000
|
-
});
|
|
24001
|
-
return;
|
|
24002
|
-
}
|
|
24003
|
-
if (sm.isSessionRunning(sessionId)) {
|
|
24004
|
-
await sendToUser({
|
|
24005
|
-
account,
|
|
24006
|
-
toUserId: fromUserId,
|
|
24007
|
-
contextToken: msg.context_token,
|
|
24008
|
-
text: t("handler.queued", lang)
|
|
24124
|
+
text: hint
|
|
24009
24125
|
});
|
|
24010
24126
|
}
|
|
24011
24127
|
if (msg.context_token)
|
|
@@ -24019,7 +24135,7 @@ async function handleInbound(args) {
|
|
|
24019
24135
|
contextToken: sessionContextTokens.get(sessionId)
|
|
24020
24136
|
}),
|
|
24021
24137
|
sendMedia: async (filePath) => {
|
|
24022
|
-
if (!
|
|
24138
|
+
if (!isMediaPathAllowed(filePath, account.workspacePath)) {
|
|
24023
24139
|
console.log(`[weixin] sendMedia blocked: ${filePath} not under workspace`);
|
|
24024
24140
|
return;
|
|
24025
24141
|
}
|
|
@@ -24061,7 +24177,7 @@ async function handleSlashCommand(args) {
|
|
|
24061
24177
|
contextToken: sessionContextTokens.get(sessionId)
|
|
24062
24178
|
}),
|
|
24063
24179
|
sendMedia: async (filePath) => {
|
|
24064
|
-
if (!
|
|
24180
|
+
if (!isMediaPathAllowed(filePath, account.workspacePath)) {
|
|
24065
24181
|
console.log(`[weixin] sendMedia blocked: ${filePath} not under workspace`);
|
|
24066
24182
|
return;
|
|
24067
24183
|
}
|
|
@@ -24143,7 +24259,7 @@ async function handleSlashCommand(args) {
|
|
|
24143
24259
|
await reply(login.message);
|
|
24144
24260
|
return true;
|
|
24145
24261
|
}
|
|
24146
|
-
const tmpPath =
|
|
24262
|
+
const tmpPath = path54.join(tempDir(), `halo-qr-${login.sessionKey}.png`);
|
|
24147
24263
|
await QRCode.toFile(tmpPath, login.qrcodeUrl, { width: 256, margin: 2 });
|
|
24148
24264
|
await sendMediaFile({
|
|
24149
24265
|
baseUrl: account.baseUrl,
|
|
@@ -24223,6 +24339,7 @@ var init_handler3 = __esm({
|
|
|
24223
24339
|
init_commands2();
|
|
24224
24340
|
init_goal_mode();
|
|
24225
24341
|
init_i18n();
|
|
24342
|
+
init_busy_hint();
|
|
24226
24343
|
init_session_prefix();
|
|
24227
24344
|
MAX_CONSECUTIVE_FAILURES = 3;
|
|
24228
24345
|
BACKOFF_DELAY_MS = 3e4;
|
|
@@ -24232,7 +24349,7 @@ var init_handler3 = __esm({
|
|
|
24232
24349
|
|
|
24233
24350
|
// packages/server/dist/routes/weixin.js
|
|
24234
24351
|
import { Hono as Hono14 } from "hono";
|
|
24235
|
-
import
|
|
24352
|
+
import path55 from "node:path";
|
|
24236
24353
|
import fs50 from "node:fs";
|
|
24237
24354
|
function createWeixinRoutes(deps) {
|
|
24238
24355
|
const { db, channel } = deps;
|
|
@@ -24258,7 +24375,7 @@ function createWeixinRoutes(deps) {
|
|
|
24258
24375
|
return c.json({ error: "sessionKey required" }, 400);
|
|
24259
24376
|
if (!body.workspacePath)
|
|
24260
24377
|
return c.json({ error: "workspacePath required" }, 400);
|
|
24261
|
-
if (!
|
|
24378
|
+
if (!path55.isAbsolute(body.workspacePath))
|
|
24262
24379
|
return c.json({ error: "workspacePath must be absolute" }, 400);
|
|
24263
24380
|
const accessLevel = body.accessLevel === "full" ? "full" : body.accessLevel === "workspace" ? "workspace" : "readonly";
|
|
24264
24381
|
const result = await waitLogin({ sessionKey: body.sessionKey, timeoutMs: body.timeoutMs });
|
|
@@ -24303,7 +24420,7 @@ function createWeixinRoutes(deps) {
|
|
|
24303
24420
|
if (!existing)
|
|
24304
24421
|
return c.json({ error: "not found" }, 404);
|
|
24305
24422
|
const body = await c.req.json().catch(() => ({}));
|
|
24306
|
-
if (body.workspacePath !== void 0 && !
|
|
24423
|
+
if (body.workspacePath !== void 0 && !path55.isAbsolute(body.workspacePath)) {
|
|
24307
24424
|
return c.json({ error: "workspacePath must be absolute" }, 400);
|
|
24308
24425
|
}
|
|
24309
24426
|
if (body.accessLevel !== void 0 && body.accessLevel !== "full" && body.accessLevel !== "workspace" && body.accessLevel !== "readonly") {
|
|
@@ -24361,7 +24478,7 @@ function readLastActiveChatId(accountId) {
|
|
|
24361
24478
|
const v = acct.config?.lastActiveChatId;
|
|
24362
24479
|
return typeof v === "string" && v.length > 0 ? v : null;
|
|
24363
24480
|
}
|
|
24364
|
-
async function dispatch2(accountId, text5, explicitChatId) {
|
|
24481
|
+
async function dispatch2(accountId, text5, explicitChatId, media) {
|
|
24365
24482
|
const acct = getAccount4(getChannelDb(), accountId);
|
|
24366
24483
|
if (!acct)
|
|
24367
24484
|
throw new Error(`wechat account ${accountId} not found`);
|
|
@@ -24371,8 +24488,32 @@ async function dispatch2(accountId, text5, explicitChatId) {
|
|
|
24371
24488
|
if (!chatId) {
|
|
24372
24489
|
throw new Error("no wechat target \u2014 bind the account first (the QR-login owner becomes the default cron recipient)");
|
|
24373
24490
|
}
|
|
24374
|
-
|
|
24375
|
-
|
|
24491
|
+
const out = [];
|
|
24492
|
+
if (text5) {
|
|
24493
|
+
await sendToUser({ account: acct, toUserId: chatId, text: text5 });
|
|
24494
|
+
out.push({ channelType: "wechat", accountId, chatId, ok: true });
|
|
24495
|
+
}
|
|
24496
|
+
for (const filePath of media?.paths ?? []) {
|
|
24497
|
+
if (!isMediaPathAllowed(filePath, media.workspacePath)) {
|
|
24498
|
+
console.log(`[weixin] cron sendMediaFile blocked: ${filePath} not under ${media.workspacePath}`);
|
|
24499
|
+
out.push({ channelType: "wechat", accountId, chatId, ok: false, error: `media path not under job workspace: ${filePath}` });
|
|
24500
|
+
continue;
|
|
24501
|
+
}
|
|
24502
|
+
try {
|
|
24503
|
+
await sendMediaFile({
|
|
24504
|
+
baseUrl: acct.baseUrl,
|
|
24505
|
+
token: acct.botToken,
|
|
24506
|
+
toUserId: chatId,
|
|
24507
|
+
filePath
|
|
24508
|
+
});
|
|
24509
|
+
out.push({ channelType: "wechat", accountId, chatId, ok: true });
|
|
24510
|
+
} catch (err) {
|
|
24511
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24512
|
+
console.log(`[weixin] cron sendMediaFile ${filePath} failed: ${msg}`);
|
|
24513
|
+
out.push({ channelType: "wechat", accountId, chatId, ok: false, error: `media ${filePath}: ${msg}` });
|
|
24514
|
+
}
|
|
24515
|
+
}
|
|
24516
|
+
return out;
|
|
24376
24517
|
}
|
|
24377
24518
|
function listTargets2() {
|
|
24378
24519
|
const cdb = getChannelDb();
|
|
@@ -24393,7 +24534,7 @@ function listTargets2() {
|
|
|
24393
24534
|
return out;
|
|
24394
24535
|
}
|
|
24395
24536
|
function registerWechatCronDispatcher() {
|
|
24396
|
-
registerCronDispatcher({ channelType: "wechat", dispatch: dispatch2, listTargets: listTargets2 });
|
|
24537
|
+
registerCronDispatcher({ channelType: "wechat", supportsMedia: true, dispatch: dispatch2, listTargets: listTargets2 });
|
|
24397
24538
|
}
|
|
24398
24539
|
var init_cron_dispatcher2 = __esm({
|
|
24399
24540
|
"packages/server/dist/channels/wechat/cron-dispatcher.js"() {
|
|
@@ -24402,6 +24543,8 @@ var init_cron_dispatcher2 = __esm({
|
|
|
24402
24543
|
init_accounts();
|
|
24403
24544
|
init_accounts4();
|
|
24404
24545
|
init_handler3();
|
|
24546
|
+
init_send_media();
|
|
24547
|
+
init_media();
|
|
24405
24548
|
init_dispatcher();
|
|
24406
24549
|
}
|
|
24407
24550
|
});
|
|
@@ -24436,14 +24579,14 @@ function createWebChannel(deps) {
|
|
|
24436
24579
|
return findActiveSessionId(sm, accountId, prefix, activeOverrides2, "full") ?? void 0;
|
|
24437
24580
|
}
|
|
24438
24581
|
function resolveWorkspace(account, override) {
|
|
24439
|
-
let
|
|
24582
|
+
let path63 = account.workspacePath;
|
|
24440
24583
|
if (override && override !== account.workspacePath) {
|
|
24441
24584
|
if (account.accessLevel !== "full") {
|
|
24442
24585
|
return { ok: false, error: "workspace override requires a full-access token" };
|
|
24443
24586
|
}
|
|
24444
|
-
|
|
24587
|
+
path63 = override;
|
|
24445
24588
|
}
|
|
24446
|
-
const resolved = resolveAccountWorkspace({ ...account, workspacePath:
|
|
24589
|
+
const resolved = resolveAccountWorkspace({ ...account, workspacePath: path63 });
|
|
24447
24590
|
if (!resolved)
|
|
24448
24591
|
return { ok: false, error: "workspace not found" };
|
|
24449
24592
|
return { ok: true, path: resolved };
|
|
@@ -24744,15 +24887,11 @@ function sseData(obj) {
|
|
|
24744
24887
|
`;
|
|
24745
24888
|
}
|
|
24746
24889
|
function flushText(text5) {
|
|
24747
|
-
const
|
|
24748
|
-
const cleaned = text5.replace(MEDIA_MARKER_RE3, (_match, p) => {
|
|
24749
|
-
files.push(p);
|
|
24750
|
-
return "";
|
|
24751
|
-
});
|
|
24890
|
+
const { text: cleaned, mediaPaths } = extractMediaPaths(text5);
|
|
24752
24891
|
let out = "";
|
|
24753
24892
|
if (cleaned)
|
|
24754
24893
|
out += sseData({ type: "stream", text: cleaned });
|
|
24755
|
-
for (const filePath of
|
|
24894
|
+
for (const filePath of mediaPaths)
|
|
24756
24895
|
out += sseData({ type: "file", path: filePath });
|
|
24757
24896
|
return out;
|
|
24758
24897
|
}
|
|
@@ -24810,7 +24949,6 @@ function createMediaBuffer() {
|
|
|
24810
24949
|
}
|
|
24811
24950
|
};
|
|
24812
24951
|
}
|
|
24813
|
-
var MEDIA_MARKER_RE3;
|
|
24814
24952
|
var init_handler4 = __esm({
|
|
24815
24953
|
"packages/server/dist/channels/web/handler.js"() {
|
|
24816
24954
|
"use strict";
|
|
@@ -24818,12 +24956,12 @@ var init_handler4 = __esm({
|
|
|
24818
24956
|
init_accounts2();
|
|
24819
24957
|
init_accounts2();
|
|
24820
24958
|
init_media_store();
|
|
24959
|
+
init_media();
|
|
24821
24960
|
init_accounts();
|
|
24822
24961
|
init_commands2();
|
|
24823
24962
|
init_goal_mode();
|
|
24824
24963
|
init_i18n();
|
|
24825
24964
|
init_session_prefix();
|
|
24826
|
-
MEDIA_MARKER_RE3 = /^MEDIA:\s*(\S.*?)\s*$/gm;
|
|
24827
24965
|
}
|
|
24828
24966
|
});
|
|
24829
24967
|
|
|
@@ -24831,7 +24969,7 @@ var init_handler4 = __esm({
|
|
|
24831
24969
|
import { Hono as Hono15 } from "hono";
|
|
24832
24970
|
import { streamSSE } from "hono/streaming";
|
|
24833
24971
|
import crypto4 from "node:crypto";
|
|
24834
|
-
import
|
|
24972
|
+
import path56 from "node:path";
|
|
24835
24973
|
import fs51 from "node:fs";
|
|
24836
24974
|
function createWebRoutes(deps) {
|
|
24837
24975
|
const { db, channel } = deps;
|
|
@@ -24857,7 +24995,7 @@ function createWebRoutes(deps) {
|
|
|
24857
24995
|
const body = await c.req.json().catch(() => ({}));
|
|
24858
24996
|
if (!body.workspacePath)
|
|
24859
24997
|
return c.json({ error: "workspacePath required" }, 400);
|
|
24860
|
-
if (!
|
|
24998
|
+
if (!path56.isAbsolute(body.workspacePath))
|
|
24861
24999
|
return c.json({ error: "workspacePath must be absolute" }, 400);
|
|
24862
25000
|
if (!fs51.existsSync(body.workspacePath))
|
|
24863
25001
|
return c.json({ error: "workspace path not found" }, 400);
|
|
@@ -24995,10 +25133,10 @@ function createWebRoutes(deps) {
|
|
|
24995
25133
|
const filePath = c.req.query("path");
|
|
24996
25134
|
if (!filePath)
|
|
24997
25135
|
return c.json({ error: "path required" }, 400);
|
|
24998
|
-
const
|
|
24999
|
-
const resolved =
|
|
25000
|
-
const root =
|
|
25001
|
-
if (resolved !== root && !resolved.startsWith(root +
|
|
25136
|
+
const path63 = await import("node:path");
|
|
25137
|
+
const resolved = path63.default.resolve(account.workspacePath, filePath);
|
|
25138
|
+
const root = path63.default.resolve(account.workspacePath);
|
|
25139
|
+
if (resolved !== root && !resolved.startsWith(root + path63.default.sep)) {
|
|
25002
25140
|
return c.json({ error: "path traversal not allowed" }, 403);
|
|
25003
25141
|
}
|
|
25004
25142
|
let real;
|
|
@@ -25009,13 +25147,13 @@ function createWebRoutes(deps) {
|
|
|
25009
25147
|
} catch {
|
|
25010
25148
|
return c.json({ error: "file not found" }, 404);
|
|
25011
25149
|
}
|
|
25012
|
-
if (real !== realRoot && !real.startsWith(realRoot +
|
|
25150
|
+
if (real !== realRoot && !real.startsWith(realRoot + path63.default.sep)) {
|
|
25013
25151
|
return c.json({ error: "path traversal not allowed" }, 403);
|
|
25014
25152
|
}
|
|
25015
25153
|
const stat = fs51.statSync(real);
|
|
25016
25154
|
if (stat.isDirectory())
|
|
25017
25155
|
return c.json({ error: "cannot serve directory" }, 400);
|
|
25018
|
-
const ext =
|
|
25156
|
+
const ext = path63.default.extname(real).toLowerCase();
|
|
25019
25157
|
const mimeMap = {
|
|
25020
25158
|
".png": "image/png",
|
|
25021
25159
|
".jpg": "image/jpeg",
|
|
@@ -25029,7 +25167,7 @@ function createWebRoutes(deps) {
|
|
|
25029
25167
|
".txt": "text/plain"
|
|
25030
25168
|
};
|
|
25031
25169
|
const contentType = mimeMap[ext] || "application/octet-stream";
|
|
25032
|
-
const fileName =
|
|
25170
|
+
const fileName = path63.default.basename(resolved);
|
|
25033
25171
|
const data = fs51.readFileSync(real);
|
|
25034
25172
|
c.header("content-type", contentType);
|
|
25035
25173
|
c.header("content-disposition", `inline; filename="${fileName}"`);
|
|
@@ -25235,13 +25373,13 @@ var init_markdown = __esm({
|
|
|
25235
25373
|
});
|
|
25236
25374
|
|
|
25237
25375
|
// packages/server/dist/channels/slack/event-adapter.js
|
|
25238
|
-
var HARD_CHARS3,
|
|
25376
|
+
var HARD_CHARS3, SlackResponder;
|
|
25239
25377
|
var init_event_adapter3 = __esm({
|
|
25240
25378
|
"packages/server/dist/channels/slack/event-adapter.js"() {
|
|
25241
25379
|
"use strict";
|
|
25242
25380
|
init_markdown();
|
|
25381
|
+
init_media();
|
|
25243
25382
|
HARD_CHARS3 = 35e3;
|
|
25244
|
-
MEDIA_MARKER_RE4 = /^MEDIA:\s*(\S.*?)\s*$/gm;
|
|
25245
25383
|
SlackResponder = class {
|
|
25246
25384
|
buffer = "";
|
|
25247
25385
|
deps;
|
|
@@ -25305,12 +25443,7 @@ var init_event_adapter3 = __esm({
|
|
|
25305
25443
|
return limit;
|
|
25306
25444
|
}
|
|
25307
25445
|
async dispatchChunk(chunk) {
|
|
25308
|
-
const mediaPaths =
|
|
25309
|
-
const stripped = chunk.replace(MEDIA_MARKER_RE4, (_m, p) => {
|
|
25310
|
-
if (p)
|
|
25311
|
-
mediaPaths.push(p.trim());
|
|
25312
|
-
return "";
|
|
25313
|
-
}).replace(/\n{3,}/g, "\n\n").trim();
|
|
25446
|
+
const { text: stripped, mediaPaths } = extractMediaMessage(chunk);
|
|
25314
25447
|
const text5 = formatForSlack(stripped);
|
|
25315
25448
|
if (text5) {
|
|
25316
25449
|
try {
|
|
@@ -25361,8 +25494,8 @@ async function authTest(botToken) {
|
|
|
25361
25494
|
}
|
|
25362
25495
|
async function uploadFile(args) {
|
|
25363
25496
|
const fs56 = await import("node:fs");
|
|
25364
|
-
const
|
|
25365
|
-
const filename = args.filename ??
|
|
25497
|
+
const path63 = await import("node:path");
|
|
25498
|
+
const filename = args.filename ?? path63.basename(args.filePath);
|
|
25366
25499
|
const stat = fs56.statSync(args.filePath);
|
|
25367
25500
|
if (!stat.isFile())
|
|
25368
25501
|
throw new Error(`[slack:uploadFile] not a regular file: ${args.filePath}`);
|
|
@@ -25526,7 +25659,7 @@ var init_api2 = __esm({
|
|
|
25526
25659
|
});
|
|
25527
25660
|
|
|
25528
25661
|
// packages/server/dist/channels/slack/handler.js
|
|
25529
|
-
import
|
|
25662
|
+
import path57 from "node:path";
|
|
25530
25663
|
import { WebSocket } from "ws";
|
|
25531
25664
|
function slashToBang(text5, commandNames) {
|
|
25532
25665
|
if (commandNames.length === 0)
|
|
@@ -25535,11 +25668,6 @@ function slashToBang(text5, commandNames) {
|
|
|
25535
25668
|
const re = new RegExp(`(^|[\\s(\\[,\uFF0C\u3001])\\/(${alternation})\\b`, "g");
|
|
25536
25669
|
return text5.replace(re, "$1!$2");
|
|
25537
25670
|
}
|
|
25538
|
-
function isPathAllowed2(filePath, workspacePath) {
|
|
25539
|
-
const resolved = path58.resolve(filePath);
|
|
25540
|
-
const wsResolved = path58.resolve(workspacePath);
|
|
25541
|
-
return resolved.startsWith(wsResolved + path58.sep) || resolved === wsResolved || isInTempDir(resolved);
|
|
25542
|
-
}
|
|
25543
25671
|
function rememberEventId(id) {
|
|
25544
25672
|
if (SEEN_EVENT_IDS.has(id))
|
|
25545
25673
|
return true;
|
|
@@ -25797,12 +25925,9 @@ async function handleInbound2(args) {
|
|
|
25797
25925
|
const sm = registry2.getOrCreate(workspace);
|
|
25798
25926
|
const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
|
|
25799
25927
|
const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateSessionForThread({ sm, channelId, rootTs, userId, accessLevel, state, workspacePath: workspace }));
|
|
25800
|
-
|
|
25801
|
-
|
|
25802
|
-
|
|
25803
|
-
}
|
|
25804
|
-
if (sm.isSessionRunning(sessionId)) {
|
|
25805
|
-
await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: "\u5DF2\u6536\u5230\uFF0C\u4F1A\u5728\u5F53\u524D\u8F6E\u7ED3\u675F\u540E\u5904\u7406\u3002" });
|
|
25928
|
+
const hint = busyHint(sm, sessionId, getLang(account));
|
|
25929
|
+
if (hint) {
|
|
25930
|
+
await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: hint });
|
|
25806
25931
|
}
|
|
25807
25932
|
if (!state.unsubscribers.has(sessionId)) {
|
|
25808
25933
|
const responder = new SlackResponder({
|
|
@@ -25810,8 +25935,8 @@ async function handleInbound2(args) {
|
|
|
25810
25935
|
await postMessage({ botToken: account.botToken, channel: channelId, threadTs: replyTs, text: chunk });
|
|
25811
25936
|
},
|
|
25812
25937
|
sendMedia: async (filePath) => {
|
|
25813
|
-
const resolved =
|
|
25814
|
-
if (!
|
|
25938
|
+
const resolved = path57.resolve(filePath);
|
|
25939
|
+
if (!isMediaPathAllowed(resolved, workspace)) {
|
|
25815
25940
|
console.log(`[slack] sendMedia blocked: ${filePath} not under workspace`);
|
|
25816
25941
|
return;
|
|
25817
25942
|
}
|
|
@@ -25828,7 +25953,10 @@ async function handleInbound2(args) {
|
|
|
25828
25953
|
botToken: account.botToken,
|
|
25829
25954
|
channel: channelId,
|
|
25830
25955
|
threadTs: replyTs,
|
|
25831
|
-
text:
|
|
25956
|
+
text: t("handler.upload_failed", getLang(account), {
|
|
25957
|
+
name: path57.basename(filePath),
|
|
25958
|
+
error: err instanceof Error ? err.message : String(err)
|
|
25959
|
+
})
|
|
25832
25960
|
}).catch(() => {
|
|
25833
25961
|
});
|
|
25834
25962
|
}
|
|
@@ -25898,6 +26026,7 @@ var init_handler5 = __esm({
|
|
|
25898
26026
|
init_session_prefix();
|
|
25899
26027
|
init_commands();
|
|
25900
26028
|
init_i18n();
|
|
26029
|
+
init_busy_hint();
|
|
25901
26030
|
SLACK_EXTRA_COMMANDS = ["qr"];
|
|
25902
26031
|
SEEN_EVENT_IDS = /* @__PURE__ */ new Set();
|
|
25903
26032
|
SEEN_EVENT_IDS_MAX = 2e3;
|
|
@@ -25907,7 +26036,7 @@ var init_handler5 = __esm({
|
|
|
25907
26036
|
// packages/server/dist/routes/slack.js
|
|
25908
26037
|
import { Hono as Hono16 } from "hono";
|
|
25909
26038
|
import fs52 from "node:fs";
|
|
25910
|
-
import
|
|
26039
|
+
import path58 from "node:path";
|
|
25911
26040
|
function createSlackRoutes(deps) {
|
|
25912
26041
|
const { db, channel } = deps;
|
|
25913
26042
|
const app2 = new Hono16();
|
|
@@ -25950,7 +26079,7 @@ function createSlackRoutes(deps) {
|
|
|
25950
26079
|
return c.json({ error: "appToken must start with xapp-" }, 400);
|
|
25951
26080
|
if (!body.workspacePath)
|
|
25952
26081
|
return c.json({ error: "workspacePath required" }, 400);
|
|
25953
|
-
if (!
|
|
26082
|
+
if (!path58.isAbsolute(body.workspacePath))
|
|
25954
26083
|
return c.json({ error: "workspacePath must be absolute" }, 400);
|
|
25955
26084
|
if (!fs52.existsSync(body.workspacePath))
|
|
25956
26085
|
return c.json({ error: "workspace path not found" }, 400);
|
|
@@ -26050,7 +26179,7 @@ function parseChatId(raw) {
|
|
|
26050
26179
|
return { channel: raw };
|
|
26051
26180
|
return { channel: raw.slice(0, idx), threadTs: raw.slice(idx + 1) || void 0 };
|
|
26052
26181
|
}
|
|
26053
|
-
async function dispatch3(accountId, text5, explicitChatId) {
|
|
26182
|
+
async function dispatch3(accountId, text5, explicitChatId, media) {
|
|
26054
26183
|
const acct = getAccount5(getChannelDb(), accountId);
|
|
26055
26184
|
if (!acct)
|
|
26056
26185
|
throw new Error(`slack account ${accountId} not found`);
|
|
@@ -26060,18 +26189,37 @@ async function dispatch3(accountId, text5, explicitChatId) {
|
|
|
26060
26189
|
throw new Error('slack cron target requires an explicit chatId (e.g. "D01234567" for a DM, or "C0123:1700.0" for a thread). Create the cron from inside a Slack chat to auto-pin, or pass --targets slack:<accountId>:<chatId>.');
|
|
26061
26190
|
}
|
|
26062
26191
|
const { channel, threadTs } = parseChatId(explicitChatId);
|
|
26063
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26067
|
-
|
|
26068
|
-
|
|
26069
|
-
|
|
26070
|
-
|
|
26071
|
-
|
|
26072
|
-
|
|
26073
|
-
|
|
26192
|
+
const out = [];
|
|
26193
|
+
if (text5) {
|
|
26194
|
+
try {
|
|
26195
|
+
await postMessage({ botToken: acct.botToken, channel, threadTs, text: text5 });
|
|
26196
|
+
out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: true });
|
|
26197
|
+
} catch (err) {
|
|
26198
|
+
out.push({
|
|
26199
|
+
channelType: "slack",
|
|
26200
|
+
accountId,
|
|
26201
|
+
chatId: explicitChatId,
|
|
26202
|
+
ok: false,
|
|
26203
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26204
|
+
});
|
|
26205
|
+
}
|
|
26206
|
+
}
|
|
26207
|
+
for (const filePath of media?.paths ?? []) {
|
|
26208
|
+
if (!isMediaPathAllowed(filePath, media.workspacePath)) {
|
|
26209
|
+
console.log(`[slack] cron uploadFile blocked: ${filePath} not under ${media.workspacePath}`);
|
|
26210
|
+
out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: false, error: `media path not under job workspace: ${filePath}` });
|
|
26211
|
+
continue;
|
|
26212
|
+
}
|
|
26213
|
+
try {
|
|
26214
|
+
await uploadFile({ botToken: acct.botToken, channel, threadTs, filePath });
|
|
26215
|
+
out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: true });
|
|
26216
|
+
} catch (err) {
|
|
26217
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
26218
|
+
console.log(`[slack] cron uploadFile ${filePath} failed: ${msg}`);
|
|
26219
|
+
out.push({ channelType: "slack", accountId, chatId: explicitChatId, ok: false, error: `media ${filePath}: ${msg}` });
|
|
26220
|
+
}
|
|
26074
26221
|
}
|
|
26222
|
+
return out;
|
|
26075
26223
|
}
|
|
26076
26224
|
function listTargets3() {
|
|
26077
26225
|
const cdb = getChannelDb();
|
|
@@ -26085,7 +26233,7 @@ function listTargets3() {
|
|
|
26085
26233
|
}));
|
|
26086
26234
|
}
|
|
26087
26235
|
function registerSlackCronDispatcher() {
|
|
26088
|
-
registerCronDispatcher({ channelType: "slack", dispatch: dispatch3, listTargets: listTargets3 });
|
|
26236
|
+
registerCronDispatcher({ channelType: "slack", supportsMedia: true, dispatch: dispatch3, listTargets: listTargets3 });
|
|
26089
26237
|
}
|
|
26090
26238
|
var init_cron_dispatcher3 = __esm({
|
|
26091
26239
|
"packages/server/dist/channels/slack/cron-dispatcher.js"() {
|
|
@@ -26093,6 +26241,7 @@ var init_cron_dispatcher3 = __esm({
|
|
|
26093
26241
|
init_channel_db();
|
|
26094
26242
|
init_accounts5();
|
|
26095
26243
|
init_api2();
|
|
26244
|
+
init_media();
|
|
26096
26245
|
init_dispatcher();
|
|
26097
26246
|
}
|
|
26098
26247
|
});
|
|
@@ -26201,13 +26350,13 @@ var init_accounts6 = __esm({
|
|
|
26201
26350
|
});
|
|
26202
26351
|
|
|
26203
26352
|
// packages/server/dist/channels/feishu/event-adapter.js
|
|
26204
|
-
var HARD_CHARS4,
|
|
26353
|
+
var HARD_CHARS4, FeishuResponder;
|
|
26205
26354
|
var init_event_adapter4 = __esm({
|
|
26206
26355
|
"packages/server/dist/channels/feishu/event-adapter.js"() {
|
|
26207
26356
|
"use strict";
|
|
26208
26357
|
init_markdown();
|
|
26358
|
+
init_media();
|
|
26209
26359
|
HARD_CHARS4 = 4500;
|
|
26210
|
-
MEDIA_MARKER_RE5 = /^MEDIA:\s*(\S.*?)\s*$/gm;
|
|
26211
26360
|
FeishuResponder = class {
|
|
26212
26361
|
buffer = "";
|
|
26213
26362
|
deps;
|
|
@@ -26271,12 +26420,7 @@ var init_event_adapter4 = __esm({
|
|
|
26271
26420
|
return limit;
|
|
26272
26421
|
}
|
|
26273
26422
|
async dispatchChunk(chunk) {
|
|
26274
|
-
const mediaPaths =
|
|
26275
|
-
const stripped = chunk.replace(MEDIA_MARKER_RE5, (_m, p) => {
|
|
26276
|
-
if (p)
|
|
26277
|
-
mediaPaths.push(p.trim());
|
|
26278
|
-
return "";
|
|
26279
|
-
}).replace(/\n{3,}/g, "\n\n").trim();
|
|
26423
|
+
const { text: stripped, mediaPaths } = extractMediaMessage(chunk);
|
|
26280
26424
|
const text5 = formatForFeishu(stripped);
|
|
26281
26425
|
if (text5) {
|
|
26282
26426
|
try {
|
|
@@ -26427,9 +26571,9 @@ async function replyMessage(args) {
|
|
|
26427
26571
|
}
|
|
26428
26572
|
async function uploadImage(args) {
|
|
26429
26573
|
const fs56 = await import("node:fs");
|
|
26430
|
-
const
|
|
26574
|
+
const path63 = await import("node:path");
|
|
26431
26575
|
const buf = fs56.readFileSync(args.filePath);
|
|
26432
|
-
const filename =
|
|
26576
|
+
const filename = path63.basename(args.filePath);
|
|
26433
26577
|
const send = async (token2) => {
|
|
26434
26578
|
const form = new FormData();
|
|
26435
26579
|
form.append("image_type", "message");
|
|
@@ -26461,9 +26605,9 @@ async function uploadImage(args) {
|
|
|
26461
26605
|
}
|
|
26462
26606
|
async function uploadFile2(args) {
|
|
26463
26607
|
const fs56 = await import("node:fs");
|
|
26464
|
-
const
|
|
26608
|
+
const path63 = await import("node:path");
|
|
26465
26609
|
const buf = fs56.readFileSync(args.filePath);
|
|
26466
|
-
const filename = args.filename ??
|
|
26610
|
+
const filename = args.filename ?? path63.basename(args.filePath);
|
|
26467
26611
|
const send = async (token2) => {
|
|
26468
26612
|
const form = new FormData();
|
|
26469
26613
|
form.append("file_type", args.fileType);
|
|
@@ -26569,13 +26713,8 @@ var init_api3 = __esm({
|
|
|
26569
26713
|
});
|
|
26570
26714
|
|
|
26571
26715
|
// packages/server/dist/channels/feishu/handler.js
|
|
26572
|
-
import
|
|
26716
|
+
import path59 from "node:path";
|
|
26573
26717
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
26574
|
-
function isPathAllowed3(filePath, workspacePath) {
|
|
26575
|
-
const resolved = path60.resolve(filePath);
|
|
26576
|
-
const wsResolved = path60.resolve(workspacePath);
|
|
26577
|
-
return resolved.startsWith(wsResolved + path60.sep) || resolved === wsResolved || isInTempDir(resolved);
|
|
26578
|
-
}
|
|
26579
26718
|
function newAccountState2() {
|
|
26580
26719
|
return {
|
|
26581
26720
|
wsClient: null,
|
|
@@ -26803,12 +26942,9 @@ async function handleInbound3(args) {
|
|
|
26803
26942
|
const sm = registry2.getOrCreate(workspace);
|
|
26804
26943
|
const accessLevel = account.accessLevel === "full" ? null : account.accessLevel === "workspace" ? "workspace" : "readonly";
|
|
26805
26944
|
const sessionId = resolveGoalRoute(sm.getDb(), await getOrCreateSessionForThread2({ sm, chatId, rootId, userId, accessLevel, state, workspacePath: workspace }));
|
|
26806
|
-
|
|
26807
|
-
|
|
26808
|
-
|
|
26809
|
-
}
|
|
26810
|
-
if (sm.isSessionRunning(sessionId)) {
|
|
26811
|
-
await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: "\u5DF2\u6536\u5230\uFF0C\u4F1A\u5728\u5F53\u524D\u8F6E\u7ED3\u675F\u540E\u5904\u7406\u3002" });
|
|
26945
|
+
const hint = busyHint(sm, sessionId, getLang(account));
|
|
26946
|
+
if (hint) {
|
|
26947
|
+
await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: hint });
|
|
26812
26948
|
}
|
|
26813
26949
|
if (!state.unsubscribers.has(sessionId)) {
|
|
26814
26950
|
const responder = new FeishuResponder({
|
|
@@ -26816,8 +26952,8 @@ async function handleInbound3(args) {
|
|
|
26816
26952
|
await replyToInbound({ account, inboundMessageId, isP2P, chatId, text: chunk });
|
|
26817
26953
|
},
|
|
26818
26954
|
sendMedia: async (filePath) => {
|
|
26819
|
-
const resolved =
|
|
26820
|
-
if (!
|
|
26955
|
+
const resolved = path59.resolve(filePath);
|
|
26956
|
+
if (!isMediaPathAllowed(resolved, workspace)) {
|
|
26821
26957
|
console.log(`[feishu] sendMedia blocked: ${filePath} not under workspace`);
|
|
26822
26958
|
return;
|
|
26823
26959
|
}
|
|
@@ -26836,7 +26972,10 @@ async function handleInbound3(args) {
|
|
|
26836
26972
|
inboundMessageId,
|
|
26837
26973
|
isP2P,
|
|
26838
26974
|
chatId,
|
|
26839
|
-
text:
|
|
26975
|
+
text: t("handler.upload_failed", getLang(account), {
|
|
26976
|
+
name: path59.basename(filePath),
|
|
26977
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26978
|
+
})
|
|
26840
26979
|
}).catch(() => {
|
|
26841
26980
|
});
|
|
26842
26981
|
}
|
|
@@ -26866,7 +27005,7 @@ async function replyToInbound(args) {
|
|
|
26866
27005
|
async function sendFeishuMedia(args) {
|
|
26867
27006
|
const { account, inboundMessageId, isP2P, chatId, filePath } = args;
|
|
26868
27007
|
const cls = classifyMedia(filePath);
|
|
26869
|
-
const ext =
|
|
27008
|
+
const ext = path59.extname(filePath).toLowerCase();
|
|
26870
27009
|
if (cls === "image") {
|
|
26871
27010
|
const { imageKey } = await uploadImage({ appId: account.appId, appSecret: account.appSecret, filePath });
|
|
26872
27011
|
await replyToInboundRaw({
|
|
@@ -27001,13 +27140,14 @@ var init_handler6 = __esm({
|
|
|
27001
27140
|
init_goal_mode();
|
|
27002
27141
|
init_session_prefix();
|
|
27003
27142
|
init_i18n();
|
|
27143
|
+
init_busy_hint();
|
|
27004
27144
|
}
|
|
27005
27145
|
});
|
|
27006
27146
|
|
|
27007
27147
|
// packages/server/dist/routes/feishu.js
|
|
27008
27148
|
import { Hono as Hono17 } from "hono";
|
|
27009
27149
|
import fs53 from "node:fs";
|
|
27010
|
-
import
|
|
27150
|
+
import path60 from "node:path";
|
|
27011
27151
|
function createFeishuRoutes(deps) {
|
|
27012
27152
|
const { db, channel } = deps;
|
|
27013
27153
|
const app2 = new Hono17();
|
|
@@ -27049,7 +27189,7 @@ function createFeishuRoutes(deps) {
|
|
|
27049
27189
|
return c.json({ error: "appSecret required" }, 400);
|
|
27050
27190
|
if (!body.workspacePath)
|
|
27051
27191
|
return c.json({ error: "workspacePath required" }, 400);
|
|
27052
|
-
if (!
|
|
27192
|
+
if (!path60.isAbsolute(body.workspacePath))
|
|
27053
27193
|
return c.json({ error: "workspacePath must be absolute" }, 400);
|
|
27054
27194
|
if (!fs53.existsSync(body.workspacePath))
|
|
27055
27195
|
return c.json({ error: "workspace path not found" }, 400);
|
|
@@ -27383,18 +27523,46 @@ function createAuthRoutes() {
|
|
|
27383
27523
|
setCookie(c, COOKIE_NAME, "", { path: "/", maxAge: 0 });
|
|
27384
27524
|
return c.json({ ok: true });
|
|
27385
27525
|
});
|
|
27526
|
+
app2.post("/auth/change-password", async (c) => {
|
|
27527
|
+
const body = await c.req.json().catch(() => ({}));
|
|
27528
|
+
const oldPassword = typeof body.oldPassword === "string" ? body.oldPassword : "";
|
|
27529
|
+
const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
|
|
27530
|
+
if (passwordEnvPlaintext() != null) {
|
|
27531
|
+
return c.json({ error: "Password is managed by the HALO_PASSWORD environment variable" }, 400);
|
|
27532
|
+
}
|
|
27533
|
+
if (!await verifyPassword(oldPassword, passwordHash())) {
|
|
27534
|
+
return c.json({ error: "Incorrect current password" }, 401);
|
|
27535
|
+
}
|
|
27536
|
+
const weak = passwordStrengthError(newPassword);
|
|
27537
|
+
if (weak)
|
|
27538
|
+
return c.json({ error: weak }, 400);
|
|
27539
|
+
if (newPassword === oldPassword) {
|
|
27540
|
+
return c.json({ error: "New password must differ from the current password" }, 400);
|
|
27541
|
+
}
|
|
27542
|
+
updateConfigLeaves({ "server.password": await hashPassword(newPassword) });
|
|
27543
|
+
return c.json({ ok: true });
|
|
27544
|
+
});
|
|
27386
27545
|
return app2;
|
|
27387
27546
|
}
|
|
27547
|
+
function passwordStrengthError(pw) {
|
|
27548
|
+
if (pw.length < 8)
|
|
27549
|
+
return "Password must be at least 8 characters";
|
|
27550
|
+
if (!/[A-Za-z]/.test(pw))
|
|
27551
|
+
return "Password must contain at least one letter";
|
|
27552
|
+
if (!/[0-9]/.test(pw))
|
|
27553
|
+
return "Password must contain at least one digit";
|
|
27554
|
+
return null;
|
|
27555
|
+
}
|
|
27388
27556
|
function authMiddleware() {
|
|
27389
27557
|
return async (c, next) => {
|
|
27390
|
-
const
|
|
27391
|
-
if (PUBLIC_PATHS.some((p) =>
|
|
27558
|
+
const path63 = c.req.path;
|
|
27559
|
+
if (PUBLIC_PATHS.some((p) => path63 === p || path63.startsWith(p + "?"))) {
|
|
27392
27560
|
return next();
|
|
27393
27561
|
}
|
|
27394
27562
|
const token = getCookie(c, COOKIE_NAME);
|
|
27395
27563
|
const payload = validateToken(token);
|
|
27396
27564
|
if (!payload) {
|
|
27397
|
-
if (!
|
|
27565
|
+
if (!path63.startsWith("/api/")) {
|
|
27398
27566
|
return next();
|
|
27399
27567
|
}
|
|
27400
27568
|
return c.json({ error: "Unauthorized" }, 401);
|
|
@@ -27412,6 +27580,7 @@ var init_auth = __esm({
|
|
|
27412
27580
|
"use strict";
|
|
27413
27581
|
init_config();
|
|
27414
27582
|
init_password_hash();
|
|
27583
|
+
init_setup_config();
|
|
27415
27584
|
init_brute_force();
|
|
27416
27585
|
TOKEN_MAX_AGE = config.auth.tokenMaxAge;
|
|
27417
27586
|
REFRESH_AFTER = config.auth.refreshAfter;
|
|
@@ -27431,7 +27600,7 @@ import { cors } from "hono/cors";
|
|
|
27431
27600
|
import { serve } from "@hono/node-server";
|
|
27432
27601
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
27433
27602
|
import { WebSocketServer } from "ws";
|
|
27434
|
-
import
|
|
27603
|
+
import path61 from "node:path";
|
|
27435
27604
|
import fs54 from "node:fs";
|
|
27436
27605
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
27437
27606
|
import { homedir as homedir24 } from "node:os";
|
|
@@ -27439,9 +27608,9 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
|
27439
27608
|
function findProjectRoot() {
|
|
27440
27609
|
let dir = process.cwd();
|
|
27441
27610
|
for (let i = 0; i < 32; i++) {
|
|
27442
|
-
if (fs54.existsSync(
|
|
27611
|
+
if (fs54.existsSync(path61.join(dir, "pnpm-workspace.yaml")))
|
|
27443
27612
|
return dir;
|
|
27444
|
-
const parent =
|
|
27613
|
+
const parent = path61.dirname(dir);
|
|
27445
27614
|
if (parent === dir)
|
|
27446
27615
|
break;
|
|
27447
27616
|
dir = parent;
|
|
@@ -27450,7 +27619,7 @@ function findProjectRoot() {
|
|
|
27450
27619
|
return process.cwd();
|
|
27451
27620
|
}
|
|
27452
27621
|
function acquireSingleInstanceLock(lockFile) {
|
|
27453
|
-
fs54.mkdirSync(
|
|
27622
|
+
fs54.mkdirSync(path61.dirname(lockFile), { recursive: true });
|
|
27454
27623
|
const fd = fs54.openSync(lockFile, fs54.constants.O_RDWR | fs54.constants.O_CREAT, 420);
|
|
27455
27624
|
const result = spawnSync4("flock", ["-n", "3"], {
|
|
27456
27625
|
stdio: ["ignore", "ignore", "ignore", fd]
|
|
@@ -27507,7 +27676,7 @@ function isProcessAlive(pid) {
|
|
|
27507
27676
|
}
|
|
27508
27677
|
}
|
|
27509
27678
|
function readGitSha() {
|
|
27510
|
-
if (!fs54.existsSync(
|
|
27679
|
+
if (!fs54.existsSync(path61.join(PROJECT_ROOT, "pnpm-workspace.yaml")) || !fs54.existsSync(path61.join(PROJECT_ROOT, ".git")))
|
|
27511
27680
|
return null;
|
|
27512
27681
|
try {
|
|
27513
27682
|
const rev = spawnSync4("git", ["rev-parse", "--short", "HEAD"], { cwd: PROJECT_ROOT, encoding: "utf-8" });
|
|
@@ -27524,14 +27693,14 @@ function readGitSha() {
|
|
|
27524
27693
|
}
|
|
27525
27694
|
function resolveFrontendDir() {
|
|
27526
27695
|
if (process.env.HALO_FRONTEND_DIR)
|
|
27527
|
-
return
|
|
27528
|
-
const bundled =
|
|
27529
|
-
if (fs54.existsSync(
|
|
27696
|
+
return path61.resolve(process.env.HALO_FRONTEND_DIR);
|
|
27697
|
+
const bundled = path61.resolve(__dirname3, "..", "admin-out");
|
|
27698
|
+
if (fs54.existsSync(path61.join(bundled, "index.html")))
|
|
27530
27699
|
return bundled;
|
|
27531
|
-
const sibling =
|
|
27532
|
-
if (fs54.existsSync(
|
|
27700
|
+
const sibling = path61.resolve(__dirname3, "..", "..", "admin", "out");
|
|
27701
|
+
if (fs54.existsSync(path61.join(sibling, "index.html")))
|
|
27533
27702
|
return sibling;
|
|
27534
|
-
return
|
|
27703
|
+
return path61.resolve(PROJECT_ROOT, "packages", "admin", "out");
|
|
27535
27704
|
}
|
|
27536
27705
|
async function gracefulShutdown(signal) {
|
|
27537
27706
|
console.log(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
@@ -27584,13 +27753,13 @@ var init_dist2 = __esm({
|
|
|
27584
27753
|
init_sandbox();
|
|
27585
27754
|
init_init();
|
|
27586
27755
|
init_git_ssh();
|
|
27587
|
-
__dirname3 =
|
|
27756
|
+
__dirname3 = path61.dirname(fileURLToPath3(import.meta.url));
|
|
27588
27757
|
PORT = config.server.port;
|
|
27589
|
-
HALO_VERSION = "1.0.
|
|
27758
|
+
HALO_VERSION = "1.0.3";
|
|
27590
27759
|
PROJECT_ROOT = findProjectRoot();
|
|
27591
|
-
HALO_HOME4 =
|
|
27760
|
+
HALO_HOME4 = path61.join(homedir24(), ".halo");
|
|
27592
27761
|
GIT_SHA = readGitSha();
|
|
27593
|
-
if (!fs54.existsSync(
|
|
27762
|
+
if (!fs54.existsSync(path61.join(HALO_HOME4, "global", ".template-version"))) {
|
|
27594
27763
|
process.stderr.write("\x1B[31m[Server] ~/.halo/global/ not initialized. Run `halo setup` first.\x1B[0m\n");
|
|
27595
27764
|
process.exit(1);
|
|
27596
27765
|
}
|
|
@@ -27611,7 +27780,7 @@ var init_dist2 = __esm({
|
|
|
27611
27780
|
process.exit(1);
|
|
27612
27781
|
}
|
|
27613
27782
|
if (!AGENTCORE)
|
|
27614
|
-
acquireSingleInstanceLock(
|
|
27783
|
+
acquireSingleInstanceLock(path61.join(HALO_HOME4, "global", "server.lock"));
|
|
27615
27784
|
{
|
|
27616
27785
|
const declared = new Set(commandRegistry.listDescriptors().filter((d) => d.source === "builtin").map((d) => d.slashName));
|
|
27617
27786
|
const dispatched = new Set(DISPATCH_COMMANDS);
|
|
@@ -27677,10 +27846,10 @@ var init_dist2 = __esm({
|
|
|
27677
27846
|
app.route("/api", evolutionRoutes);
|
|
27678
27847
|
cronRoutes = createCronRoutes();
|
|
27679
27848
|
app.route("/api", cronRoutes);
|
|
27680
|
-
channelDb = createChannelDb(
|
|
27849
|
+
channelDb = createChannelDb(path61.join(HALO_HOME4, "secrets"));
|
|
27681
27850
|
setChannelDb(channelDb);
|
|
27682
|
-
setEvoDb(createEvoDb(
|
|
27683
|
-
setCronDb(createCronDb(
|
|
27851
|
+
setEvoDb(createEvoDb(path61.join(HALO_HOME4, "global")));
|
|
27852
|
+
setCronDb(createCronDb(path61.join(HALO_HOME4, "global")));
|
|
27684
27853
|
setEvoSpawner(realEvoSpawner);
|
|
27685
27854
|
if (!AGENTCORE) {
|
|
27686
27855
|
startEvoTicker();
|
|
@@ -27704,9 +27873,9 @@ var init_dist2 = __esm({
|
|
|
27704
27873
|
console.log(`[Server] AgentCore runtime mode \u2014 workspace: ${config.server.agentcoreWorkspace}`);
|
|
27705
27874
|
}
|
|
27706
27875
|
FRONTEND_DIR = resolveFrontendDir();
|
|
27707
|
-
app.use("/*", serveStatic({ root:
|
|
27876
|
+
app.use("/*", serveStatic({ root: path61.relative(process.cwd(), FRONTEND_DIR) }));
|
|
27708
27877
|
app.get("/*", (c) => {
|
|
27709
|
-
const indexPath =
|
|
27878
|
+
const indexPath = path61.join(FRONTEND_DIR, "index.html");
|
|
27710
27879
|
try {
|
|
27711
27880
|
const html = fs54.readFileSync(indexPath, "utf-8");
|
|
27712
27881
|
return c.html(html);
|
|
@@ -28477,11 +28646,11 @@ process.emitWarning = (warning, ...args) => {
|
|
|
28477
28646
|
};
|
|
28478
28647
|
|
|
28479
28648
|
// packages/cli/src/index.ts
|
|
28480
|
-
import
|
|
28649
|
+
import path62 from "node:path";
|
|
28481
28650
|
import fs55 from "node:fs";
|
|
28482
28651
|
import { homedir as homedir25 } from "node:os";
|
|
28483
28652
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
28484
|
-
var VERSION = "1.0.
|
|
28653
|
+
var VERSION = "1.0.3";
|
|
28485
28654
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
28486
28655
|
|
|
28487
28656
|
Commands:
|
|
@@ -28734,8 +28903,8 @@ async function cmdSetup(argv = []) {
|
|
|
28734
28903
|
const { buildBindProviderPlan: buildBindProviderPlan2, KEEP_CURRENT: KEEP_CURRENT2, AWS_CHAIN_PROVIDER: AWS_CHAIN_PROVIDER2 } = await Promise.resolve().then(() => (init_setup_bind_provider(), setup_bind_provider_exports));
|
|
28735
28904
|
const { promptText: promptText2, promptPassword: promptPassword2, promptSelect: promptSelect2, promptMultiSelect: promptMultiSelect2 } = await Promise.resolve().then(() => (init_setup_prompts(), setup_prompts_exports));
|
|
28736
28905
|
const nonInteractive = argv.includes("--non-interactive") || argv.includes("-y");
|
|
28737
|
-
const haloHome2 =
|
|
28738
|
-
const globalDir2 =
|
|
28906
|
+
const haloHome2 = path62.join(homedir25(), ".halo");
|
|
28907
|
+
const globalDir2 = path62.join(haloHome2, "global");
|
|
28739
28908
|
ensureHaloHome2(haloHome2);
|
|
28740
28909
|
process.stderr.write(`[setup] ${haloHome2}/ ready
|
|
28741
28910
|
`);
|
|
@@ -29079,7 +29248,7 @@ halo: upgrade failed (npm exited ${install.status}). ${hint}
|
|
|
29079
29248
|
async function cmdAgents(flags) {
|
|
29080
29249
|
const { initRuntime: initRuntime2, listAgents: listAgents2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
29081
29250
|
await initRuntime2();
|
|
29082
|
-
const workspace =
|
|
29251
|
+
const workspace = path62.resolve(flags.workspace ?? process.cwd());
|
|
29083
29252
|
const agents = await listAgents2(workspace);
|
|
29084
29253
|
if (agents.length === 0) {
|
|
29085
29254
|
process.stderr.write("No agents found.\n");
|
|
@@ -29095,7 +29264,7 @@ async function cmdAgents(flags) {
|
|
|
29095
29264
|
async function cmdSessions(flags) {
|
|
29096
29265
|
const { initRuntime: initRuntime2, listSessions: listSessions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
29097
29266
|
await initRuntime2();
|
|
29098
|
-
const workspace =
|
|
29267
|
+
const workspace = path62.resolve(flags.workspace ?? process.cwd());
|
|
29099
29268
|
const sessions2 = listSessions2(workspace);
|
|
29100
29269
|
if (sessions2.length === 0) {
|
|
29101
29270
|
process.stderr.write("No sessions found.\n");
|
|
@@ -29197,8 +29366,8 @@ ${stdinContent}` : stdinContent;
|
|
|
29197
29366
|
harness.destroy();
|
|
29198
29367
|
}
|
|
29199
29368
|
}
|
|
29200
|
-
var SERVER_LOCK_PATH =
|
|
29201
|
-
var SERVER_LOG_PATH =
|
|
29369
|
+
var SERVER_LOCK_PATH = path62.join(homedir25(), ".halo", "global", "server.lock");
|
|
29370
|
+
var SERVER_LOG_PATH = path62.join(homedir25(), ".halo", "logs", "server.log");
|
|
29202
29371
|
function readServerPid() {
|
|
29203
29372
|
try {
|
|
29204
29373
|
const txt = fs55.readFileSync(SERVER_LOCK_PATH, "utf-8").trim();
|
|
@@ -29283,7 +29452,7 @@ async function cmdServerStart(argv, _unused) {
|
|
|
29283
29452
|
return;
|
|
29284
29453
|
}
|
|
29285
29454
|
const { spawn: spawn4 } = await import("node:child_process");
|
|
29286
|
-
fs55.mkdirSync(
|
|
29455
|
+
fs55.mkdirSync(path62.dirname(SERVER_LOG_PATH), { recursive: true });
|
|
29287
29456
|
const out = fs55.openSync(SERVER_LOG_PATH, "a");
|
|
29288
29457
|
const child = spawn4(process.argv[0], [process.argv[1], "server", "start"], {
|
|
29289
29458
|
detached: true,
|
|
@@ -29402,7 +29571,7 @@ function exitMissingSetup() {
|
|
|
29402
29571
|
process.exit(1);
|
|
29403
29572
|
}
|
|
29404
29573
|
function isHaloHomeReady() {
|
|
29405
|
-
return fs55.existsSync(
|
|
29574
|
+
return fs55.existsSync(path62.join(homedir25(), ".halo", "global", ".template-version"));
|
|
29406
29575
|
}
|
|
29407
29576
|
async function main2() {
|
|
29408
29577
|
const argv = process.argv.slice(2);
|