sanook-cli 0.5.1 → 0.5.5
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/.env.example +161 -3
- package/CHANGELOG.md +148 -10
- package/README.md +255 -26
- package/README.th.md +95 -7
- package/dist/approval.js +13 -0
- package/dist/bin.js +3552 -155
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +262 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +377 -0
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +15 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +190 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +266 -27
- package/dist/compaction.js +96 -11
- package/dist/config.js +149 -33
- package/dist/context-compression.js +191 -0
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +49 -15
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +49 -9
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +399 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +501 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +38 -1
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +362 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +55 -0
- package/dist/insights.js +86 -0
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +157 -29
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +494 -0
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +120 -10
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +148 -37
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +51 -19
- package/dist/personality.js +58 -0
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +89 -43
- package/dist/providers/keys.js +22 -1
- package/dist/providers/models.js +2 -2
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +83 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session-distill.js +84 -0
- package/dist/session.js +92 -16
- package/dist/skill-install.js +53 -13
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +206 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +992 -12
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/read.js +16 -4
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +226 -15
- package/dist/tools/task.js +40 -9
- package/dist/tools/timeout.js +23 -3
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/trust.js +11 -1
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +878 -32
- package/dist/ui/banner.js +78 -4
- package/dist/ui/history.js +37 -5
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +20 -1
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +172 -46
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +56 -17
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/dist/worktree.js +175 -4
- package/package.json +5 -5
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +19 -4
- package/second-brain/Projects/sanook-cli/_Index.md +30 -0
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +8 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -4
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/gateway/serve.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { mkdir } from 'node:fs/promises';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
2
|
import { acquireSingleton } from './lock.js';
|
|
4
|
-
import { loadOrCreateToken } from './auth.js';
|
|
3
|
+
import { ensureGatewayDir, loadOrCreateToken } from './auth.js';
|
|
5
4
|
import { startServer } from './server.js';
|
|
6
5
|
import { startScheduler } from './scheduler.js';
|
|
7
6
|
import { appHomePath, BRAND, BRAND_ENV, envFlag } from '../brand.js';
|
|
7
|
+
import { readGatewayConfig, resolveDiscordConfig, resolveEmailConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveWhatsAppConfig, resolveWebhookConfig, } from './config.js';
|
|
8
8
|
const GATEWAY_DIR = appHomePath('gateway');
|
|
9
9
|
const SERVE_LOCK = join(GATEWAY_DIR, 'serve.lock');
|
|
10
10
|
/**
|
|
@@ -15,7 +15,7 @@ const SERVE_LOCK = join(GATEWAY_DIR, 'serve.lock');
|
|
|
15
15
|
*/
|
|
16
16
|
export async function startGateway(opts) {
|
|
17
17
|
const log = opts.onLog ?? ((m) => console.log(`[gateway] ${m}`));
|
|
18
|
-
await
|
|
18
|
+
await ensureGatewayDir();
|
|
19
19
|
const release = await acquireSingleton(SERVE_LOCK);
|
|
20
20
|
if (!release) {
|
|
21
21
|
throw new Error(`มี ${BRAND.cliName} gateway รันอยู่แล้ว (เจอ serve.lock) — ปิดตัวเดิมก่อน หรือถ้าค้างให้ลบ ${appHomePath('gateway', 'serve.lock')}`);
|
|
@@ -35,25 +35,285 @@ export async function startGateway(opts) {
|
|
|
35
35
|
permissionMode: opts.permissionMode ?? (envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : 'ask'),
|
|
36
36
|
tickMs: opts.tickMs,
|
|
37
37
|
onLog: log,
|
|
38
|
+
deliver: async (task, output) => {
|
|
39
|
+
if (!task.deliver)
|
|
40
|
+
return;
|
|
41
|
+
const { deliverToTarget } = await import('./deliver.js');
|
|
42
|
+
const result = await deliverToTarget(task.deliver, output, { subject: `${BRAND.productName} task ${task.id}` });
|
|
43
|
+
log(`delivered ${task.id} → ${result.target}`);
|
|
44
|
+
},
|
|
38
45
|
});
|
|
39
|
-
// Telegram channel (
|
|
46
|
+
// Telegram channel (env หรือ ~/.sanook/gateway/config.json) — long-polling, ไม่ต้อง public URL
|
|
40
47
|
let stopTelegram;
|
|
41
|
-
|
|
48
|
+
let stopDiscord;
|
|
49
|
+
let stopSlack;
|
|
50
|
+
let stopMattermost;
|
|
51
|
+
let stopHomeAssistant;
|
|
52
|
+
let stopEmail;
|
|
53
|
+
let stopNtfy;
|
|
54
|
+
let stopSignal;
|
|
55
|
+
let stopMatrix;
|
|
56
|
+
const gatewayConfig = await readGatewayConfig();
|
|
57
|
+
const telegram = resolveTelegramConfig(gatewayConfig);
|
|
58
|
+
if (telegram.enabled && telegram.token) {
|
|
42
59
|
const { startTelegram, parseAllowedChats } = await import('./telegram.js');
|
|
43
60
|
stopTelegram = startTelegram({
|
|
44
|
-
token:
|
|
61
|
+
token: telegram.token,
|
|
45
62
|
model: opts.model,
|
|
46
63
|
budgetUsd: opts.budgetUsd,
|
|
47
|
-
allowedChatIds: parseAllowedChats(process.env.TELEGRAM_ALLOWED_CHATS),
|
|
64
|
+
allowedChatIds: process.env.TELEGRAM_ALLOWED_CHATS ? parseAllowedChats(process.env.TELEGRAM_ALLOWED_CHATS) : telegram.allowedChatIds,
|
|
65
|
+
allowWrite: telegram.allowWrite,
|
|
48
66
|
onLog: log,
|
|
49
67
|
});
|
|
50
68
|
// หมายเหตุ: log "เริ่มแล้ว" อยู่ใน startTelegram (success path) — ถ้า fail-closed จะ log "ไม่เริ่ม" แทน
|
|
51
69
|
}
|
|
70
|
+
const discord = resolveDiscordConfig(gatewayConfig);
|
|
71
|
+
if (discord.enabled && discord.token) {
|
|
72
|
+
const { startDiscord } = await import('./discord.js');
|
|
73
|
+
try {
|
|
74
|
+
stopDiscord = startDiscord({
|
|
75
|
+
token: discord.token,
|
|
76
|
+
model: opts.model,
|
|
77
|
+
budgetUsd: opts.budgetUsd,
|
|
78
|
+
allowedChannelIds: discord.allowedChannelIds,
|
|
79
|
+
defaultChannelId: discord.defaultChannelId,
|
|
80
|
+
allowWrite: discord.allowWrite,
|
|
81
|
+
onLog: log,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
log(`Discord ไม่เริ่ม: ${e.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const slack = resolveSlackConfig(gatewayConfig);
|
|
89
|
+
if (slack.enabled && slack.botToken) {
|
|
90
|
+
if (!slack.appToken) {
|
|
91
|
+
log('Slack ไม่เริ่ม: ต้องตั้ง SLACK_APP_TOKEN หรือ gateway setup slack --app-token สำหรับ Socket Mode');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const { startSlack } = await import('./slack.js');
|
|
95
|
+
try {
|
|
96
|
+
stopSlack = await startSlack({
|
|
97
|
+
botToken: slack.botToken,
|
|
98
|
+
appToken: slack.appToken,
|
|
99
|
+
model: opts.model,
|
|
100
|
+
budgetUsd: opts.budgetUsd,
|
|
101
|
+
allowedChannelIds: slack.allowedChannelIds,
|
|
102
|
+
defaultChannelId: slack.defaultChannelId,
|
|
103
|
+
allowWrite: slack.allowWrite,
|
|
104
|
+
onLog: log,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
log(`Slack ไม่เริ่ม: ${e.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const mattermost = resolveMattermostConfig(gatewayConfig);
|
|
113
|
+
if (mattermost.enabled && (mattermost.serverUrl || mattermost.token || mattermost.homeChannel || mattermost.allowedUsers.length || mattermost.allowedChannels.length)) {
|
|
114
|
+
if (!mattermost.serverUrl) {
|
|
115
|
+
log('Mattermost ไม่เริ่ม: ต้องตั้ง MATTERMOST_URL เช่น https://mm.example.com');
|
|
116
|
+
}
|
|
117
|
+
else if (!mattermost.token) {
|
|
118
|
+
log('Mattermost ไม่เริ่ม: ต้องตั้ง MATTERMOST_TOKEN');
|
|
119
|
+
}
|
|
120
|
+
else if (!mattermost.allowAllUsers && !mattermost.allowedUsers.length) {
|
|
121
|
+
log('Mattermost ไม่เริ่ม: ต้องตั้ง MATTERMOST_ALLOWED_USERS เพื่อ fail-closed');
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const { startMattermost } = await import('./mattermost.js');
|
|
125
|
+
try {
|
|
126
|
+
stopMattermost = await startMattermost({
|
|
127
|
+
config: mattermost,
|
|
128
|
+
model: opts.model,
|
|
129
|
+
budgetUsd: opts.budgetUsd,
|
|
130
|
+
permissionMode: opts.permissionMode ?? (envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : 'ask'),
|
|
131
|
+
onLog: log,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
log(`Mattermost ไม่เริ่ม: ${e.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const homeassistant = resolveHomeAssistantConfig(gatewayConfig);
|
|
140
|
+
if (homeassistant.enabled && (homeassistant.token || homeassistant.homeChannel || homeassistant.watchAll || homeassistant.watchDomains.length || homeassistant.watchEntities.length)) {
|
|
141
|
+
if (!homeassistant.token) {
|
|
142
|
+
log('Home Assistant ไม่เริ่ม: ต้องตั้ง HASS_TOKEN');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const { startHomeAssistant } = await import('./homeassistant.js');
|
|
146
|
+
try {
|
|
147
|
+
stopHomeAssistant = startHomeAssistant({
|
|
148
|
+
config: homeassistant,
|
|
149
|
+
model: opts.model,
|
|
150
|
+
budgetUsd: opts.budgetUsd,
|
|
151
|
+
permissionMode: opts.permissionMode ?? (envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : 'ask'),
|
|
152
|
+
onLog: log,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
log(`Home Assistant ไม่เริ่ม: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const email = resolveEmailConfig(gatewayConfig);
|
|
161
|
+
if (email.enabled && email.address) {
|
|
162
|
+
if (!email.password || !email.imapHost || !email.smtpHost) {
|
|
163
|
+
log('Email ไม่เริ่ม: ต้องตั้ง password, imapHost และ smtpHost ให้ครบ');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const { startEmail } = await import('./email.js');
|
|
167
|
+
stopEmail = startEmail({
|
|
168
|
+
address: email.address,
|
|
169
|
+
password: email.password,
|
|
170
|
+
imapHost: email.imapHost,
|
|
171
|
+
imapPort: email.imapPort,
|
|
172
|
+
smtpHost: email.smtpHost,
|
|
173
|
+
smtpPort: email.smtpPort,
|
|
174
|
+
homeAddress: email.homeAddress,
|
|
175
|
+
allowedUsers: email.allowedUsers,
|
|
176
|
+
allowAllUsers: email.allowAllUsers,
|
|
177
|
+
pollIntervalSeconds: email.pollIntervalSeconds,
|
|
178
|
+
model: opts.model,
|
|
179
|
+
budgetUsd: opts.budgetUsd,
|
|
180
|
+
allowWrite: false,
|
|
181
|
+
onLog: log,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const line = resolveLineConfig(gatewayConfig);
|
|
186
|
+
if (line.enabled && line.channelAccessToken) {
|
|
187
|
+
if (!line.channelSecret) {
|
|
188
|
+
log('LINE webhook ไม่เริ่ม: ต้องตั้ง LINE_CHANNEL_SECRET หรือ gateway setup line --channel-secret');
|
|
189
|
+
}
|
|
190
|
+
else if (!line.homeChannel && !line.allowedUsers.length && !line.allowedGroups.length && !line.allowedRooms.length && !line.allowAllUsers) {
|
|
191
|
+
log('LINE webhook ไม่เริ่ม: ต้องตั้ง home channel หรือ allowlist เพื่อ fail-closed');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const publicBase = line.publicUrl ? `${line.publicUrl.replace(/\/+$/, '')}/line/webhook` : `http://127.0.0.1:${opts.port}/line/webhook`;
|
|
195
|
+
log(`LINE: webhook ready at ${publicBase}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const sms = resolveSmsConfig(gatewayConfig);
|
|
199
|
+
if (sms.enabled && (sms.accountSid || sms.authToken || sms.phoneNumber)) {
|
|
200
|
+
if (!sms.accountSid || !sms.authToken || !sms.phoneNumber) {
|
|
201
|
+
log('SMS webhook ไม่เริ่ม: ต้องตั้ง TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN และ TWILIO_PHONE_NUMBER ให้ครบ');
|
|
202
|
+
}
|
|
203
|
+
else if (!sms.insecureNoSignature && !sms.webhookUrl) {
|
|
204
|
+
log('SMS webhook ไม่เริ่ม: ต้องตั้ง SMS_WEBHOOK_URL ให้ตรงกับ Twilio Console เพื่อ verify signature');
|
|
205
|
+
}
|
|
206
|
+
else if (!sms.homeChannel && !sms.allowedUsers.length && !sms.allowAllUsers) {
|
|
207
|
+
log('SMS webhook ไม่เริ่ม: ต้องตั้ง home channel หรือ allowlist เพื่อ fail-closed');
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const publicBase = sms.webhookUrl || `http://127.0.0.1:${opts.port}/sms/webhook`;
|
|
211
|
+
log(`SMS: Twilio webhook ready at ${publicBase}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const ntfy = resolveNtfyConfig(gatewayConfig);
|
|
215
|
+
if (ntfy.enabled && (ntfy.topic || ntfy.publishTopic || ntfy.homeChannel || ntfy.token)) {
|
|
216
|
+
if (!ntfy.topic) {
|
|
217
|
+
log('ntfy ไม่เริ่ม: ต้องตั้ง NTFY_TOPIC หรือ gateway setup ntfy --topic สำหรับ inbound subscribe');
|
|
218
|
+
}
|
|
219
|
+
else if (!ntfy.allowAllUsers && ![ntfy.topic, ntfy.homeChannel, ...ntfy.allowedUsers].filter(Boolean).includes(ntfy.topic)) {
|
|
220
|
+
log('ntfy ไม่เริ่ม: ต้องตั้ง NTFY_ALLOWED_USERS ให้รวม topic หรือระบุ --allow-all-users เพื่อ fail-closed');
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const { startNtfy } = await import('./ntfy.js');
|
|
224
|
+
stopNtfy = startNtfy({
|
|
225
|
+
config: ntfy,
|
|
226
|
+
model: opts.model,
|
|
227
|
+
budgetUsd: opts.budgetUsd,
|
|
228
|
+
permissionMode: opts.permissionMode ?? (envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : 'ask'),
|
|
229
|
+
onLog: log,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const signal = resolveSignalConfig(gatewayConfig);
|
|
234
|
+
if (signal.enabled && (signal.account || signal.homeChannel || signal.allowedUsers.length || signal.groupAllowedUsers.length)) {
|
|
235
|
+
if (!signal.account) {
|
|
236
|
+
log('Signal ไม่เริ่ม: ต้องตั้ง SIGNAL_ACCOUNT หรือ gateway setup signal --account <+E.164>');
|
|
237
|
+
}
|
|
238
|
+
else if (!signal.allowAllUsers && !signal.homeChannel && !signal.allowedUsers.length && !signal.groupAllowedUsers.length) {
|
|
239
|
+
log('Signal ไม่เริ่ม: ต้องตั้ง home channel หรือ allowlist เพื่อ fail-closed');
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const { startSignal } = await import('./signal.js');
|
|
243
|
+
stopSignal = startSignal({
|
|
244
|
+
config: signal,
|
|
245
|
+
model: opts.model,
|
|
246
|
+
budgetUsd: opts.budgetUsd,
|
|
247
|
+
permissionMode: opts.permissionMode ?? (envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : 'ask'),
|
|
248
|
+
onLog: log,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const whatsapp = resolveWhatsAppConfig(gatewayConfig);
|
|
253
|
+
if (whatsapp.enabled && (whatsapp.phoneNumberId || whatsapp.accessToken || whatsapp.homeChannel || whatsapp.allowedUsers.length)) {
|
|
254
|
+
if (!whatsapp.phoneNumberId || !whatsapp.accessToken) {
|
|
255
|
+
log('WhatsApp Cloud webhook ไม่เริ่ม: ต้องตั้ง WHATSAPP_CLOUD_PHONE_NUMBER_ID และ WHATSAPP_CLOUD_ACCESS_TOKEN ให้ครบ');
|
|
256
|
+
}
|
|
257
|
+
else if (!whatsapp.appSecret) {
|
|
258
|
+
log('WhatsApp Cloud webhook ไม่เริ่ม: ต้องตั้ง WHATSAPP_CLOUD_APP_SECRET เพื่อ verify X-Hub-Signature-256');
|
|
259
|
+
}
|
|
260
|
+
else if (!whatsapp.verifyToken) {
|
|
261
|
+
log('WhatsApp Cloud webhook ไม่เริ่ม: ต้องตั้ง WHATSAPP_CLOUD_VERIFY_TOKEN สำหรับ Meta webhook verify handshake');
|
|
262
|
+
}
|
|
263
|
+
else if (!whatsapp.homeChannel && !whatsapp.allowedUsers.length && !whatsapp.allowAllUsers) {
|
|
264
|
+
log('WhatsApp Cloud webhook ไม่เริ่ม: ต้องตั้ง home channel หรือ allowlist เพื่อ fail-closed');
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const publicBase = whatsapp.publicUrl ? `${whatsapp.publicUrl.replace(/\/+$/, '')}/whatsapp/webhook` : `http://127.0.0.1:${opts.port}/whatsapp/webhook`;
|
|
268
|
+
log(`WhatsApp Cloud: webhook ready at ${publicBase}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const matrix = resolveMatrixConfig(gatewayConfig);
|
|
272
|
+
if (matrix.enabled && (matrix.homeserver || matrix.accessToken || matrix.userId || matrix.homeRoom || matrix.allowedUsers.length || matrix.allowedRooms.length)) {
|
|
273
|
+
if (!matrix.homeserver) {
|
|
274
|
+
log('Matrix ไม่เริ่ม: ต้องตั้ง MATRIX_HOMESERVER เช่น https://matrix.org');
|
|
275
|
+
}
|
|
276
|
+
else if (!matrix.accessToken && (!matrix.userId || !matrix.password)) {
|
|
277
|
+
log('Matrix ไม่เริ่ม: ต้องตั้ง MATRIX_ACCESS_TOKEN หรือ MATRIX_USER_ID/MATRIX_PASSWORD');
|
|
278
|
+
}
|
|
279
|
+
else if (!matrix.allowAllUsers && !matrix.allowedUsers.length) {
|
|
280
|
+
log('Matrix ไม่เริ่ม: ต้องตั้ง MATRIX_ALLOWED_USERS เพื่อ fail-closed');
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const { startMatrix } = await import('./matrix.js');
|
|
284
|
+
stopMatrix = startMatrix({
|
|
285
|
+
config: matrix,
|
|
286
|
+
model: opts.model,
|
|
287
|
+
budgetUsd: opts.budgetUsd,
|
|
288
|
+
permissionMode: opts.permissionMode ?? (envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : 'ask'),
|
|
289
|
+
onLog: log,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const webhooks = resolveWebhookConfig(gatewayConfig);
|
|
294
|
+
if (webhooks.enabled) {
|
|
295
|
+
const routes = Object.keys(webhooks.routes);
|
|
296
|
+
if (!routes.length) {
|
|
297
|
+
log('Webhooks เปิดอยู่ แต่ยังไม่มี route — เพิ่มด้วย sanook webhook subscribe <name>');
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const base = webhooks.publicUrl ? `${webhooks.publicUrl.replace(/\/+$/, '')}/webhooks` : `http://127.0.0.1:${opts.port}/webhooks`;
|
|
301
|
+
log(`Webhooks: ${routes.length} route(s) ready at ${base}/<route>`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
52
304
|
log(`scheduler tick ทุก ${(opts.tickMs ?? 60_000) / 1000}s · token: ${appHomePath('gateway', 'token')} (chmod 600)`);
|
|
53
305
|
return () => {
|
|
54
306
|
stopServer();
|
|
55
307
|
stopScheduler();
|
|
56
308
|
stopTelegram?.();
|
|
309
|
+
stopDiscord?.();
|
|
310
|
+
stopSlack?.();
|
|
311
|
+
stopMattermost?.();
|
|
312
|
+
stopHomeAssistant?.();
|
|
313
|
+
stopEmail?.();
|
|
314
|
+
stopNtfy?.();
|
|
315
|
+
stopSignal?.();
|
|
316
|
+
stopMatrix?.();
|
|
57
317
|
release(); // ปล่อย single-instance lock (sync — ทันก่อน process.exit ตัด event loop)
|
|
58
318
|
};
|
|
59
319
|
}
|
package/dist/gateway/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { listTasks, enqueueTask } from './ledger.js';
|
|
3
3
|
import { parseSchedule } from './schedule.js';
|
|
4
|
+
import { formatTarget, parseSendTarget } from './targets.js';
|
|
4
5
|
import { tokenMatches } from './auth.js';
|
|
5
6
|
import { runAgent } from '../loop.js';
|
|
6
7
|
import { redactKey } from '../providers/keys.js';
|
|
@@ -9,21 +10,111 @@ function send(res, status, body) {
|
|
|
9
10
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
10
11
|
res.end(JSON.stringify(body));
|
|
11
12
|
}
|
|
13
|
+
function sendRaw(res, status, contentType, body) {
|
|
14
|
+
res.writeHead(status, { 'content-type': contentType });
|
|
15
|
+
res.end(body);
|
|
16
|
+
}
|
|
17
|
+
function sendSse(res, body) {
|
|
18
|
+
res.write(`data: ${typeof body === 'string' ? body : JSON.stringify(body)}\n\n`);
|
|
19
|
+
}
|
|
20
|
+
export function optionalString(value) {
|
|
21
|
+
if (typeof value !== 'string')
|
|
22
|
+
return undefined;
|
|
23
|
+
const trimmed = value.trim();
|
|
24
|
+
return trimmed || undefined;
|
|
25
|
+
}
|
|
26
|
+
export function parseBearerToken(authorization) {
|
|
27
|
+
if (!authorization)
|
|
28
|
+
return undefined;
|
|
29
|
+
const tokenMatch = /^Bearer +(\S+)$/i.exec(authorization);
|
|
30
|
+
if (!tokenMatch)
|
|
31
|
+
return undefined;
|
|
32
|
+
return tokenMatch[1];
|
|
33
|
+
}
|
|
34
|
+
export function parseWebhookRouteName(pathname) {
|
|
35
|
+
if (!pathname.startsWith('/webhooks/'))
|
|
36
|
+
return undefined;
|
|
37
|
+
try {
|
|
38
|
+
const routeName = decodeURIComponent(pathname.slice('/webhooks/'.length)).replace(/^\/+|\/+$/g, '');
|
|
39
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(routeName) ? routeName : undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function parseOptionalSchedule(value, now) {
|
|
46
|
+
if (value == null)
|
|
47
|
+
return { schedule: null };
|
|
48
|
+
if (typeof value !== 'string')
|
|
49
|
+
return { schedule: null, invalid: 'ต้องเป็นข้อความ' };
|
|
50
|
+
const scheduleText = optionalString(value);
|
|
51
|
+
if (!scheduleText)
|
|
52
|
+
return { schedule: null };
|
|
53
|
+
const schedule = parseSchedule(scheduleText, now);
|
|
54
|
+
return schedule ? { schedule } : { schedule: null, invalid: scheduleText };
|
|
55
|
+
}
|
|
56
|
+
export function parseRequiredTaskSpec(value) {
|
|
57
|
+
if (typeof value !== 'string') {
|
|
58
|
+
return value == null ? { invalid: 'ต้องมี spec' } : { invalid: 'spec ต้องเป็นข้อความ' };
|
|
59
|
+
}
|
|
60
|
+
const spec = value.trim();
|
|
61
|
+
return spec ? { spec } : { invalid: 'ต้องมี spec' };
|
|
62
|
+
}
|
|
63
|
+
export function parseOptionalDeliverTarget(value) {
|
|
64
|
+
if (value == null)
|
|
65
|
+
return {};
|
|
66
|
+
if (typeof value !== 'string')
|
|
67
|
+
return { invalid: 'deliver ต้องเป็นข้อความ' };
|
|
68
|
+
const deliverText = optionalString(value);
|
|
69
|
+
if (!deliverText)
|
|
70
|
+
return {};
|
|
71
|
+
try {
|
|
72
|
+
return { deliver: formatTarget(parseSendTarget(deliverText)) };
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return { invalid: e.message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function parseOptionalTaskModel(value) {
|
|
79
|
+
if (value == null)
|
|
80
|
+
return {};
|
|
81
|
+
if (typeof value !== 'string')
|
|
82
|
+
return { invalid: 'model ต้องเป็นข้อความ' };
|
|
83
|
+
const model = optionalString(value);
|
|
84
|
+
return model ? { model } : {};
|
|
85
|
+
}
|
|
12
86
|
const MAX_BODY = 1_000_000; // 1MB กัน memory blowup
|
|
87
|
+
/** error ที่พก HTTP status — ให้ client เห็น 400/413 (client error) แทน 500 (server error) */
|
|
88
|
+
class HttpError extends Error {
|
|
89
|
+
status;
|
|
90
|
+
constructor(status, message) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.status = status;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
13
95
|
async function readBody(req) {
|
|
96
|
+
const raw = await readRawBody(req);
|
|
97
|
+
if (!raw)
|
|
98
|
+
return {};
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new HttpError(400, 'invalid JSON body'); // Bad Request — ไม่ leak ข้อความ parser
|
|
105
|
+
}
|
|
106
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
107
|
+
}
|
|
108
|
+
async function readRawBody(req) {
|
|
14
109
|
const chunks = [];
|
|
15
110
|
let size = 0;
|
|
16
111
|
for await (const c of req) {
|
|
17
112
|
size += c.length;
|
|
18
113
|
if (size > MAX_BODY)
|
|
19
|
-
throw new
|
|
114
|
+
throw new HttpError(413, 'request body ใหญ่เกิน'); // Payload Too Large
|
|
20
115
|
chunks.push(c);
|
|
21
116
|
}
|
|
22
|
-
|
|
23
|
-
if (!raw)
|
|
24
|
-
return {};
|
|
25
|
-
const parsed = JSON.parse(raw);
|
|
26
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
117
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
27
118
|
}
|
|
28
119
|
/**
|
|
29
120
|
* gateway HTTP — bind 127.0.0.1 เท่านั้น (loopback, ไม่ expose ออกเน็ต), ทุก endpoint ยกเว้น /health ต้อง bearer token
|
|
@@ -33,7 +124,7 @@ async function readBody(req) {
|
|
|
33
124
|
export function startServer(opts) {
|
|
34
125
|
const server = createServer((req, res) => {
|
|
35
126
|
// redact กัน API key/secret รั่วใน error response (provider error อาจฝัง key)
|
|
36
|
-
void handle(req, res, opts).catch((err) => send(res, 500, { error: redactKey(err.message ?? String(err)) }));
|
|
127
|
+
void handle(req, res, opts).catch((err) => send(res, err.status ?? 500, { error: redactKey(err.message ?? String(err)) }));
|
|
37
128
|
});
|
|
38
129
|
// '127.0.0.1' = loopback only — สำคัญ: ห้าม 0.0.0.0 (จะเปิดให้ทั้ง LAN)
|
|
39
130
|
server.listen(opts.port, '127.0.0.1', () => opts.onLog?.(`http://127.0.0.1:${opts.port} (loopback)`));
|
|
@@ -45,9 +136,102 @@ async function handle(req, res, opts) {
|
|
|
45
136
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
46
137
|
return send(res, 200, { ok: true, service: BRAND.gatewayServiceName });
|
|
47
138
|
}
|
|
139
|
+
if (req.method === 'GET' && url.pathname === '/line/webhook/health') {
|
|
140
|
+
return send(res, 200, { status: 'ok', platform: 'line' });
|
|
141
|
+
}
|
|
142
|
+
if (req.method === 'GET' && url.pathname === '/sms/webhook/health') {
|
|
143
|
+
return send(res, 200, { status: 'ok', platform: 'sms' });
|
|
144
|
+
}
|
|
145
|
+
if (req.method === 'GET' && url.pathname === '/whatsapp/webhook/health') {
|
|
146
|
+
const { readGatewayConfig, resolveWhatsAppConfig } = await import('./config.js');
|
|
147
|
+
const whatsapp = resolveWhatsAppConfig(await readGatewayConfig());
|
|
148
|
+
return send(res, 200, {
|
|
149
|
+
status: 'ok',
|
|
150
|
+
platform: 'whatsapp',
|
|
151
|
+
phone_number_id_configured: Boolean(whatsapp.phoneNumberId),
|
|
152
|
+
access_token_configured: Boolean(whatsapp.accessToken),
|
|
153
|
+
app_secret_configured: Boolean(whatsapp.appSecret),
|
|
154
|
+
verify_token_configured: Boolean(whatsapp.verifyToken),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (req.method === 'GET' && url.pathname === '/webhooks/health') {
|
|
158
|
+
return send(res, 200, { status: 'ok', platform: 'webhook' });
|
|
159
|
+
}
|
|
160
|
+
if (req.method === 'POST' && url.pathname === '/line/webhook') {
|
|
161
|
+
const rawBody = await readRawBody(req);
|
|
162
|
+
const signature = Array.isArray(req.headers['x-line-signature']) ? req.headers['x-line-signature'][0] : req.headers['x-line-signature'];
|
|
163
|
+
const { readGatewayConfig, resolveLineConfig } = await import('./config.js');
|
|
164
|
+
const { handleLineWebhook } = await import('./line.js');
|
|
165
|
+
const result = await handleLineWebhook({
|
|
166
|
+
rawBody,
|
|
167
|
+
signature,
|
|
168
|
+
config: resolveLineConfig(await readGatewayConfig()),
|
|
169
|
+
model: opts.defaultModel,
|
|
170
|
+
budgetUsd: opts.budgetUsd,
|
|
171
|
+
permissionMode: opts.permissionMode ?? 'ask',
|
|
172
|
+
onLog: opts.onLog,
|
|
173
|
+
});
|
|
174
|
+
return send(res, result.status, result.body);
|
|
175
|
+
}
|
|
176
|
+
if (req.method === 'POST' && url.pathname === '/sms/webhook') {
|
|
177
|
+
const rawBody = await readRawBody(req);
|
|
178
|
+
const signature = Array.isArray(req.headers['x-twilio-signature']) ? req.headers['x-twilio-signature'][0] : req.headers['x-twilio-signature'];
|
|
179
|
+
const { readGatewayConfig, resolveSmsConfig } = await import('./config.js');
|
|
180
|
+
const { handleSmsWebhook } = await import('./sms.js');
|
|
181
|
+
const result = await handleSmsWebhook({
|
|
182
|
+
rawBody,
|
|
183
|
+
signature,
|
|
184
|
+
config: resolveSmsConfig(await readGatewayConfig()),
|
|
185
|
+
model: opts.defaultModel,
|
|
186
|
+
budgetUsd: opts.budgetUsd,
|
|
187
|
+
permissionMode: opts.permissionMode ?? 'ask',
|
|
188
|
+
onLog: opts.onLog,
|
|
189
|
+
});
|
|
190
|
+
return sendRaw(res, result.status, result.contentType, result.body);
|
|
191
|
+
}
|
|
192
|
+
if (req.method === 'GET' && url.pathname === '/whatsapp/webhook') {
|
|
193
|
+
const { readGatewayConfig, resolveWhatsAppConfig } = await import('./config.js');
|
|
194
|
+
const { handleWhatsAppChallenge } = await import('./whatsapp.js');
|
|
195
|
+
const result = handleWhatsAppChallenge(resolveWhatsAppConfig(await readGatewayConfig()), url.searchParams);
|
|
196
|
+
return sendRaw(res, result.status, result.contentType, result.body);
|
|
197
|
+
}
|
|
198
|
+
if (req.method === 'POST' && url.pathname === '/whatsapp/webhook') {
|
|
199
|
+
const rawBody = await readRawBody(req);
|
|
200
|
+
const signature = Array.isArray(req.headers['x-hub-signature-256']) ? req.headers['x-hub-signature-256'][0] : req.headers['x-hub-signature-256'];
|
|
201
|
+
const { readGatewayConfig, resolveWhatsAppConfig } = await import('./config.js');
|
|
202
|
+
const { handleWhatsAppWebhook } = await import('./whatsapp.js');
|
|
203
|
+
const result = await handleWhatsAppWebhook({
|
|
204
|
+
rawBody,
|
|
205
|
+
signature,
|
|
206
|
+
config: resolveWhatsAppConfig(await readGatewayConfig()),
|
|
207
|
+
model: opts.defaultModel,
|
|
208
|
+
budgetUsd: opts.budgetUsd,
|
|
209
|
+
permissionMode: opts.permissionMode ?? 'ask',
|
|
210
|
+
onLog: opts.onLog,
|
|
211
|
+
});
|
|
212
|
+
return send(res, result.status, result.body);
|
|
213
|
+
}
|
|
214
|
+
if (req.method === 'POST' && url.pathname.startsWith('/webhooks/')) {
|
|
215
|
+
const routeName = parseWebhookRouteName(url.pathname);
|
|
216
|
+
if (!routeName)
|
|
217
|
+
return send(res, 400, { error: 'invalid_webhook_route' });
|
|
218
|
+
const rawBody = await readRawBody(req);
|
|
219
|
+
const { readGatewayConfig, resolveWebhookConfig } = await import('./config.js');
|
|
220
|
+
const { handleWebhookRequest } = await import('./webhooks.js');
|
|
221
|
+
const result = await handleWebhookRequest({
|
|
222
|
+
routeName,
|
|
223
|
+
rawBody,
|
|
224
|
+
headers: req.headers,
|
|
225
|
+
config: resolveWebhookConfig(await readGatewayConfig()),
|
|
226
|
+
model: opts.defaultModel,
|
|
227
|
+
budgetUsd: opts.budgetUsd,
|
|
228
|
+
permissionMode: opts.permissionMode ?? 'ask',
|
|
229
|
+
onLog: opts.onLog,
|
|
230
|
+
});
|
|
231
|
+
return send(res, result.status, result.body);
|
|
232
|
+
}
|
|
48
233
|
// ทุก endpoint อื่น → bearer token
|
|
49
|
-
const
|
|
50
|
-
const provided = auth.startsWith('Bearer ') ? auth.slice(7) : undefined;
|
|
234
|
+
const provided = parseBearerToken(req.headers.authorization);
|
|
51
235
|
if (!tokenMatches(opts.token, provided)) {
|
|
52
236
|
return send(res, 401, { error: 'unauthorized' });
|
|
53
237
|
}
|
|
@@ -63,8 +247,51 @@ async function handle(req, res, opts) {
|
|
|
63
247
|
const history = msgs
|
|
64
248
|
.slice(0, lastUserIdx)
|
|
65
249
|
.map((m) => ({ role: m.role, content: m.content }));
|
|
66
|
-
const
|
|
67
|
-
|
|
250
|
+
const { model: requestedModel, invalid: invalidModel } = parseOptionalTaskModel(body.model);
|
|
251
|
+
if (invalidModel)
|
|
252
|
+
return send(res, 400, { error: invalidModel });
|
|
253
|
+
const model = requestedModel ?? opts.defaultModel;
|
|
254
|
+
const runner = opts.runner ?? runAgent;
|
|
255
|
+
if (body.stream === true) {
|
|
256
|
+
res.writeHead(200, {
|
|
257
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
258
|
+
'cache-control': 'no-cache, no-transform',
|
|
259
|
+
connection: 'keep-alive',
|
|
260
|
+
});
|
|
261
|
+
sendSse(res, {
|
|
262
|
+
object: 'chat.completion.chunk',
|
|
263
|
+
model,
|
|
264
|
+
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
await runner({
|
|
268
|
+
model,
|
|
269
|
+
prompt,
|
|
270
|
+
history,
|
|
271
|
+
maxSteps: 20,
|
|
272
|
+
budgetUsd: opts.budgetUsd,
|
|
273
|
+
permissionMode: opts.permissionMode ?? 'ask',
|
|
274
|
+
onEvent: (e) => {
|
|
275
|
+
if (e.type !== 'text' || !e.text)
|
|
276
|
+
return;
|
|
277
|
+
sendSse(res, {
|
|
278
|
+
object: 'chat.completion.chunk',
|
|
279
|
+
model,
|
|
280
|
+
choices: [{ index: 0, delta: { content: e.text }, finish_reason: null }],
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
sendSse(res, { object: 'chat.completion.chunk', model, choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
|
|
285
|
+
sendSse(res, '[DONE]');
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
sendSse(res, { error: redactKey(e.message ?? String(e)) });
|
|
289
|
+
sendSse(res, '[DONE]');
|
|
290
|
+
}
|
|
291
|
+
res.end();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const { text } = await runner({
|
|
68
295
|
model,
|
|
69
296
|
prompt,
|
|
70
297
|
history,
|
|
@@ -83,17 +310,24 @@ async function handle(req, res, opts) {
|
|
|
83
310
|
}
|
|
84
311
|
if (req.method === 'POST' && url.pathname === '/tasks') {
|
|
85
312
|
const body = await readBody(req);
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
return send(res, 400, { error:
|
|
89
|
-
const sched
|
|
90
|
-
if (
|
|
91
|
-
return send(res, 400, { error: `schedule ไม่ถูกต้อง: ${
|
|
313
|
+
const specInput = parseRequiredTaskSpec(body.spec);
|
|
314
|
+
if ('invalid' in specInput)
|
|
315
|
+
return send(res, 400, { error: specInput.invalid });
|
|
316
|
+
const { schedule: sched, invalid } = parseOptionalSchedule(body.schedule, Date.now());
|
|
317
|
+
if (invalid)
|
|
318
|
+
return send(res, 400, { error: `schedule ไม่ถูกต้อง: ${invalid}` });
|
|
319
|
+
const { model, invalid: invalidModel } = parseOptionalTaskModel(body.model);
|
|
320
|
+
if (invalidModel)
|
|
321
|
+
return send(res, 400, { error: invalidModel });
|
|
322
|
+
const { deliver, invalid: invalidDeliver } = parseOptionalDeliverTarget(body.deliver);
|
|
323
|
+
if (invalidDeliver)
|
|
324
|
+
return send(res, 400, { error: invalidDeliver });
|
|
92
325
|
const task = await enqueueTask({
|
|
93
326
|
kind: sched?.recurring ? 'cron' : 'once',
|
|
94
|
-
spec,
|
|
327
|
+
spec: specInput.spec,
|
|
95
328
|
schedule: sched?.recurring ? sched.normalized : undefined,
|
|
96
|
-
model
|
|
329
|
+
model,
|
|
330
|
+
deliver,
|
|
97
331
|
runAt: sched?.runAt ?? Date.now(),
|
|
98
332
|
});
|
|
99
333
|
return send(res, 201, { task });
|