@tianshu-ai/tianshu 0.3.52 → 0.3.53
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/package.json +1 -1
- package/packages/server/dist/boot/idle-runner.d.ts +15 -0
- package/packages/server/dist/boot/idle-runner.d.ts.map +1 -0
- package/packages/server/dist/boot/idle-runner.js +118 -0
- package/packages/server/dist/boot/idle-runner.js.map +1 -0
- package/packages/server/dist/boot/routes-channels.d.ts +15 -0
- package/packages/server/dist/boot/routes-channels.d.ts.map +1 -0
- package/packages/server/dist/boot/routes-channels.js +150 -0
- package/packages/server/dist/boot/routes-channels.js.map +1 -0
- package/packages/server/dist/boot/routes-core.d.ts +10 -0
- package/packages/server/dist/boot/routes-core.d.ts.map +1 -0
- package/packages/server/dist/boot/routes-core.js +106 -0
- package/packages/server/dist/boot/routes-core.js.map +1 -0
- package/packages/server/dist/boot/static-spa.d.ts +12 -0
- package/packages/server/dist/boot/static-spa.d.ts.map +1 -0
- package/packages/server/dist/boot/static-spa.js +86 -0
- package/packages/server/dist/boot/static-spa.js.map +1 -0
- package/packages/server/dist/boot/ws-upgrade.d.ts +16 -0
- package/packages/server/dist/boot/ws-upgrade.d.ts.map +1 -0
- package/packages/server/dist/boot/ws-upgrade.js +115 -0
- package/packages/server/dist/boot/ws-upgrade.js.map +1 -0
- package/packages/server/dist/channels/index.d.ts +2 -0
- package/packages/server/dist/channels/index.d.ts.map +1 -1
- package/packages/server/dist/channels/index.js +1 -0
- package/packages/server/dist/channels/index.js.map +1 -1
- package/packages/server/dist/channels/router.js +17 -49
- package/packages/server/dist/channels/router.js.map +1 -1
- package/packages/server/dist/channels/stream-sink.d.ts +22 -0
- package/packages/server/dist/channels/stream-sink.d.ts.map +1 -0
- package/packages/server/dist/channels/stream-sink.js +79 -0
- package/packages/server/dist/channels/stream-sink.js.map +1 -0
- package/packages/server/dist/chat/agent-loop.d.ts.map +1 -1
- package/packages/server/dist/chat/agent-loop.js +2 -1
- package/packages/server/dist/chat/agent-loop.js.map +1 -1
- package/packages/server/dist/chat/handler.d.ts.map +1 -1
- package/packages/server/dist/chat/handler.js +2 -1
- package/packages/server/dist/chat/handler.js.map +1 -1
- package/packages/server/dist/chat/require-home-dir.d.ts +7 -0
- package/packages/server/dist/chat/require-home-dir.d.ts.map +1 -0
- package/packages/server/dist/chat/require-home-dir.js +40 -0
- package/packages/server/dist/chat/require-home-dir.js.map +1 -0
- package/packages/server/dist/index.js +39 -536
- package/packages/server/dist/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GlobalOps } from "../core/global-ops.js";
|
|
2
|
+
import type { PluginRegistry } from "../core/plugins/registry.js";
|
|
3
|
+
export interface InstallIdleRunnerDeps {
|
|
4
|
+
globalOps: GlobalOps;
|
|
5
|
+
pluginRegistry: PluginRegistry;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Install the idle-runner. Idempotent in practice (bindIdleRunner is
|
|
9
|
+
* a one-shot module setter), but conceptually call once at boot.
|
|
10
|
+
*
|
|
11
|
+
* Deps are captured by closure so this module doesn't need a
|
|
12
|
+
* mutable singleton.
|
|
13
|
+
*/
|
|
14
|
+
export declare function installIdleRunner(deps: InstallIdleRunnerDeps): void;
|
|
15
|
+
//# sourceMappingURL=idle-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle-runner.d.ts","sourceRoot":"","sources":["../../src/boot/idle-runner.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAGlE,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;CAChC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CAyGnE"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Wire the session-inbox idle runner.
|
|
2
|
+
//
|
|
3
|
+
// When a worker pool finishes a task and the parent chat session is
|
|
4
|
+
// idle (no active harness), the inbox module kicks a background
|
|
5
|
+
// `runPrompt` turn so the agent reacts to the notification
|
|
6
|
+
// immediately instead of waiting for the user to send something.
|
|
7
|
+
//
|
|
8
|
+
// We bind once at startup (`bindIdleRunner` is process-wide) so the
|
|
9
|
+
// inbox module doesn't have to import the host registry directly. The
|
|
10
|
+
// runner closure resolves the tenant from globalOps at call time \u2014
|
|
11
|
+
// the session row already lives in some tenant DB; the closure is
|
|
12
|
+
// tenant-agnostic.
|
|
13
|
+
//
|
|
14
|
+
// Two delivery paths matter inside the runner:
|
|
15
|
+
// - webchat session \u2192 broadcastToUser to every open WS tab
|
|
16
|
+
// - channel-bound session \u2192 buildChannelStreamSink \u2192 channelHub
|
|
17
|
+
//
|
|
18
|
+
// Without that split a background turn on a wechat-bound session ends
|
|
19
|
+
// up only on the user's webchat tabs (if any) and never crosses back
|
|
20
|
+
// to the platform user. See memory/2026-06-27 for the original bug.
|
|
21
|
+
import { bindIdleRunner } from "../chat/session-inbox.js";
|
|
22
|
+
import { runPrompt } from "../chat/handler.js";
|
|
23
|
+
import { broadcastToUser } from "../chat/active-harnesses.js";
|
|
24
|
+
import { buildChannelStreamSink, channelHub } from "../channels/index.js";
|
|
25
|
+
/**
|
|
26
|
+
* Install the idle-runner. Idempotent in practice (bindIdleRunner is
|
|
27
|
+
* a one-shot module setter), but conceptually call once at boot.
|
|
28
|
+
*
|
|
29
|
+
* Deps are captured by closure so this module doesn't need a
|
|
30
|
+
* mutable singleton.
|
|
31
|
+
*/
|
|
32
|
+
export function installIdleRunner(deps) {
|
|
33
|
+
const { globalOps, pluginRegistry } = deps;
|
|
34
|
+
bindIdleRunner(async ({ sessionId, userId, promptText }) => {
|
|
35
|
+
// Find which tenant owns this session, and pick up the channel
|
|
36
|
+
// tagging columns at the same time so we know whether to fan
|
|
37
|
+
// replies out to WS tabs (webchat) or push them through the
|
|
38
|
+
// channel adapter (wechat / telegram / ...). If no tenant
|
|
39
|
+
// claims this session, give up silently \u2014 the inbox row stays
|
|
40
|
+
// delivered=false and will be flushed on the next user prompt.
|
|
41
|
+
let owningCtx = null;
|
|
42
|
+
let channelBindingId = null;
|
|
43
|
+
let channelChatId = null;
|
|
44
|
+
for (const tenantId of globalOps.list()) {
|
|
45
|
+
const ctx = globalOps.open(tenantId);
|
|
46
|
+
const row = ctx.db
|
|
47
|
+
.prepare(`SELECT id, channel_binding_id, channel_chat_id
|
|
48
|
+
FROM sessions WHERE id = ?`)
|
|
49
|
+
.get(sessionId);
|
|
50
|
+
if (row) {
|
|
51
|
+
owningCtx = ctx;
|
|
52
|
+
channelBindingId = row.channel_binding_id;
|
|
53
|
+
channelChatId = row.channel_chat_id;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!owningCtx) {
|
|
58
|
+
console.warn(`[idle-runner] no tenant found for session ${sessionId}; skipping`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const ctx = owningCtx;
|
|
62
|
+
const isChannelSession = channelBindingId !== null && channelChatId !== null;
|
|
63
|
+
const channelSink = isChannelSession
|
|
64
|
+
? buildChannelStreamSink({ sessionId, userId })
|
|
65
|
+
: null;
|
|
66
|
+
let errorReason = "";
|
|
67
|
+
const send = (msg) => {
|
|
68
|
+
if (channelSink) {
|
|
69
|
+
channelSink.push(msg);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
broadcastToUser(userId, msg);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
// Wedge guard: a stuck provider can't pin the inbox queue
|
|
76
|
+
// forever. No user-facing abort button for an inbox turn \u2014
|
|
77
|
+
// generous deadline only.
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const deadline = setTimeout(() => controller.abort(), 5 * 60 * 1000);
|
|
80
|
+
try {
|
|
81
|
+
await runPrompt({
|
|
82
|
+
ctx,
|
|
83
|
+
userId,
|
|
84
|
+
send,
|
|
85
|
+
content: promptText,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
pluginRegistry,
|
|
88
|
+
homeDir: ctx.workspaceDir,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
errorReason = err instanceof Error ? err.message : String(err);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(deadline);
|
|
96
|
+
}
|
|
97
|
+
if (channelSink && channelBindingId && channelChatId) {
|
|
98
|
+
const sinkError = channelSink.getErrorReason();
|
|
99
|
+
if (sinkError)
|
|
100
|
+
errorReason = sinkError;
|
|
101
|
+
for (const body of channelSink.assistantQueue) {
|
|
102
|
+
try {
|
|
103
|
+
await channelHub.send(channelBindingId, {
|
|
104
|
+
target: channelChatId,
|
|
105
|
+
text: body,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.error(`[idle-runner] adapter send failed (${channelBindingId} \u2192 ${channelChatId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (errorReason.length > 0) {
|
|
113
|
+
console.warn(`[idle-runner] background turn errored on channel session ${sessionId}: ${errorReason}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=idle-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idle-runner.js","sourceRoot":"","sources":["../../src/boot/idle-runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { ChannelBinding } from "../channels/index.js";
|
|
3
|
+
import type { ChannelBindingView } from "@tianshu-ai/plugin-sdk";
|
|
4
|
+
/** Map an internal binding row to the SDK-shaped view. Same wire
|
|
5
|
+
* shape host.channelBindings.list() emits, so plugin UIs and admin
|
|
6
|
+
* routes don't diverge. */
|
|
7
|
+
export declare function toView(row: ChannelBinding): ChannelBindingView;
|
|
8
|
+
/**
|
|
9
|
+
* Mount all `/api/channel-{sessions,bindings}/*` routes on the
|
|
10
|
+
* provided Express app. Call once during boot, after the tenant
|
|
11
|
+
* middleware has been installed (these routes assume `req.ctx`
|
|
12
|
+
* exists).
|
|
13
|
+
*/
|
|
14
|
+
export declare function mountChannelRoutes(app: Express): void;
|
|
15
|
+
//# sourceMappingURL=routes-channels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes-channels.d.ts","sourceRoot":"","sources":["../../src/boot/routes-channels.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;4BAE4B;AAC5B,wBAAgB,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,kBAAkB,CAe9D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAUrD"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Channel admin routes: read-only `/api/channel-sessions/*` for the
|
|
2
|
+
// sidebar + UI controls, plus `PATCH /api/channel-bindings/:id/model`
|
|
3
|
+
// so a user can flip the LLM behind their wechat / telegram thread
|
|
4
|
+
// without re-binding.
|
|
5
|
+
//
|
|
6
|
+
// These all live behind the tenant middleware so `req.ctx` is
|
|
7
|
+
// available; each route enforces user scoping (sessions.user_id /
|
|
8
|
+
// bindings.owner_user_id) so two users in the same tenant can't read
|
|
9
|
+
// each other's channel state.
|
|
10
|
+
import { getBinding, updateBinding } from "../channels/index.js";
|
|
11
|
+
/** Map an internal binding row to the SDK-shaped view. Same wire
|
|
12
|
+
* shape host.channelBindings.list() emits, so plugin UIs and admin
|
|
13
|
+
* routes don't diverge. */
|
|
14
|
+
export function toView(row) {
|
|
15
|
+
return {
|
|
16
|
+
id: row.id,
|
|
17
|
+
tenantId: row.tenantId,
|
|
18
|
+
ownerUserId: row.ownerUserId,
|
|
19
|
+
channelId: row.channelId,
|
|
20
|
+
pluginId: row.pluginId,
|
|
21
|
+
displayName: row.displayName,
|
|
22
|
+
enabled: row.enabled,
|
|
23
|
+
status: row.status,
|
|
24
|
+
statusDetail: row.statusDetail,
|
|
25
|
+
config: row.config,
|
|
26
|
+
createdAt: row.createdAt,
|
|
27
|
+
updatedAt: row.updatedAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Mount all `/api/channel-{sessions,bindings}/*` routes on the
|
|
32
|
+
* provided Express app. Call once during boot, after the tenant
|
|
33
|
+
* middleware has been installed (these routes assume `req.ctx`
|
|
34
|
+
* exists).
|
|
35
|
+
*/
|
|
36
|
+
export function mountChannelRoutes(app) {
|
|
37
|
+
app.get("/api/channel-sessions", listChannelSessions);
|
|
38
|
+
app.get("/api/channel-sessions/:sessionId/binding", getChannelSessionBinding);
|
|
39
|
+
app.patch("/api/channel-bindings/:bindingId/model", patchChannelBindingModel);
|
|
40
|
+
}
|
|
41
|
+
function listChannelSessions(req, res) {
|
|
42
|
+
if (!req.ctx) {
|
|
43
|
+
res.status(500).json({ error: "no_ctx" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Scope to the calling user: channel sessions are personal (one
|
|
47
|
+
// user's wechat scan shouldn't surface to other users on the same
|
|
48
|
+
// tenant). user_id on the session row already tracks this because
|
|
49
|
+
// ensureChannelSession stamps it from the binding's owner_user_id.
|
|
50
|
+
const rows = req.ctx.tenant.db
|
|
51
|
+
.prepare(`SELECT id, channel_id, channel_chat_id, channel_binding_id,
|
|
52
|
+
title, created_at
|
|
53
|
+
FROM sessions
|
|
54
|
+
WHERE channel_id IS NOT NULL
|
|
55
|
+
AND kind = 'user'
|
|
56
|
+
AND user_id = ?
|
|
57
|
+
ORDER BY created_at DESC`)
|
|
58
|
+
.all(req.ctx.userId);
|
|
59
|
+
res.json({
|
|
60
|
+
sessions: rows.map((r) => ({
|
|
61
|
+
id: r.id,
|
|
62
|
+
channelId: r.channel_id,
|
|
63
|
+
channelChatId: r.channel_chat_id,
|
|
64
|
+
channelBindingId: r.channel_binding_id,
|
|
65
|
+
title: r.title,
|
|
66
|
+
createdAt: r.created_at,
|
|
67
|
+
})),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Return the binding row a channel session belongs to. Channel UIs
|
|
72
|
+
* use this to populate per-session controls (model selector, etc.)
|
|
73
|
+
* keyed off the session the user is viewing. Filtered by user_id so
|
|
74
|
+
* users can't read each other's bindings.
|
|
75
|
+
*/
|
|
76
|
+
function getChannelSessionBinding(req, res) {
|
|
77
|
+
if (!req.ctx) {
|
|
78
|
+
res.status(500).json({ error: "no_ctx" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const rawSid = req.params.sessionId;
|
|
82
|
+
const sessionId = Array.isArray(rawSid) ? rawSid[0] : rawSid;
|
|
83
|
+
if (!sessionId) {
|
|
84
|
+
res.status(400).json({ error: "missing session id" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const row = req.ctx.tenant.db
|
|
88
|
+
.prepare(`SELECT channel_binding_id AS binding_id
|
|
89
|
+
FROM sessions
|
|
90
|
+
WHERE id = ? AND user_id = ?
|
|
91
|
+
LIMIT 1`)
|
|
92
|
+
.get(sessionId, req.ctx.userId);
|
|
93
|
+
if (!row?.binding_id) {
|
|
94
|
+
res.status(404).json({ error: "no binding for session" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const binding = getBinding(req.ctx.tenant.db, row.binding_id);
|
|
98
|
+
if (!binding ||
|
|
99
|
+
binding.tenantId !== req.ctx.tenant.tenantId ||
|
|
100
|
+
binding.ownerUserId !== req.ctx.userId) {
|
|
101
|
+
res.status(404).json({ error: "binding not found" });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
res.json({
|
|
105
|
+
binding: {
|
|
106
|
+
id: binding.id,
|
|
107
|
+
channelId: binding.channelId,
|
|
108
|
+
modelId: typeof binding.config.modelId === "string" &&
|
|
109
|
+
binding.config.modelId.trim().length > 0
|
|
110
|
+
? binding.config.modelId.trim()
|
|
111
|
+
: null,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Update a binding's model. PATCH so the body is the partial we
|
|
117
|
+
* want to merge. Only `modelId` is patchable through this route
|
|
118
|
+
* today; future channel-config edits can land on the same surface.
|
|
119
|
+
*/
|
|
120
|
+
function patchChannelBindingModel(req, res) {
|
|
121
|
+
if (!req.ctx) {
|
|
122
|
+
res.status(500).json({ error: "no_ctx" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const rawBid = req.params.bindingId;
|
|
126
|
+
const bindingId = Array.isArray(rawBid) ? rawBid[0] : rawBid;
|
|
127
|
+
if (!bindingId) {
|
|
128
|
+
res.status(400).json({ error: "missing binding id" });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const body = (req.body ?? {});
|
|
132
|
+
const newModelId = typeof body.modelId === "string" && body.modelId.trim().length > 0
|
|
133
|
+
? body.modelId.trim()
|
|
134
|
+
: null;
|
|
135
|
+
const binding = getBinding(req.ctx.tenant.db, bindingId);
|
|
136
|
+
if (!binding ||
|
|
137
|
+
binding.tenantId !== req.ctx.tenant.tenantId ||
|
|
138
|
+
binding.ownerUserId !== req.ctx.userId) {
|
|
139
|
+
res.status(404).json({ error: "binding not found" });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const nextConfig = { ...binding.config };
|
|
143
|
+
if (newModelId)
|
|
144
|
+
nextConfig.modelId = newModelId;
|
|
145
|
+
else
|
|
146
|
+
delete nextConfig.modelId;
|
|
147
|
+
updateBinding(req.ctx.tenant.db, bindingId, { config: nextConfig });
|
|
148
|
+
res.json({ ok: true, modelId: newModelId });
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=routes-channels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes-channels.js","sourceRoot":"","sources":["../../src/boot/routes-channels.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { PluginRegistry } from "../core/plugins/registry.js";
|
|
3
|
+
export interface MountCoreRoutesDeps {
|
|
4
|
+
/** Registry used by /api/tools and /api/skills to enumerate the
|
|
5
|
+
* current tenant's catalog. Closures resolve it at request time
|
|
6
|
+
* so plugin enable/disable cycles take effect immediately. */
|
|
7
|
+
pluginRegistry: PluginRegistry;
|
|
8
|
+
}
|
|
9
|
+
export declare function mountCoreRoutes(app: Express, deps: MountCoreRoutesDeps): void;
|
|
10
|
+
//# sourceMappingURL=routes-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes-core.d.ts","sourceRoot":"","sources":["../../src/boot/routes-core.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAG1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,MAAM,WAAW,mBAAmB;IAClC;;mEAE+D;IAC/D,cAAc,EAAE,cAAc,CAAC;CAChC;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,mBAAmB,GACxB,IAAI,CA0GN"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Core read-only API: `/api/me`, `/api/models`, `/api/tools`,
|
|
2
|
+
// `/api/skills`. These power the chat shell's identity badge,
|
|
3
|
+
// model picker, and worker-agents allow-list pickers.
|
|
4
|
+
//
|
|
5
|
+
// All four assume tenant middleware has run (req.ctx is set); each
|
|
6
|
+
// scopes to the calling user when appropriate. Side-effect-free \u2014
|
|
7
|
+
// no DB writes, no plugin state mutation.
|
|
8
|
+
import { DEV_TENANT_ID, DEV_USER_ID } from "../core/dev-mode.js";
|
|
9
|
+
import { listModels, getDefaultModel } from "../core/llm.js";
|
|
10
|
+
export function mountCoreRoutes(app, deps) {
|
|
11
|
+
const { pluginRegistry } = deps;
|
|
12
|
+
app.get("/api/me", (req, res) => {
|
|
13
|
+
if (!req.ctx) {
|
|
14
|
+
res.status(500).json({ error: "no_ctx" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const { tenant, userId } = req.ctx;
|
|
18
|
+
const def = getDefaultModel(tenant.config);
|
|
19
|
+
res.json({
|
|
20
|
+
tenantId: tenant.tenantId,
|
|
21
|
+
userId,
|
|
22
|
+
config: { branding: tenant.config.branding ?? null },
|
|
23
|
+
defaultModel: def
|
|
24
|
+
? { id: def.id, name: def.name, provider: def.providerId }
|
|
25
|
+
: null,
|
|
26
|
+
devTenant: tenant.tenantId === DEV_TENANT_ID && userId === DEV_USER_ID,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
app.get("/api/models", (req, res) => {
|
|
30
|
+
if (!req.ctx) {
|
|
31
|
+
res.status(500).json({ error: "no_ctx" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const list = listModels(req.ctx.tenant.config).map((m) => ({
|
|
35
|
+
id: m.id,
|
|
36
|
+
name: m.name,
|
|
37
|
+
provider: m.providerId,
|
|
38
|
+
group: m.group ?? null,
|
|
39
|
+
contextWindow: m.contextWindow,
|
|
40
|
+
reasoning: m.reasoning,
|
|
41
|
+
}));
|
|
42
|
+
res.json({
|
|
43
|
+
models: list,
|
|
44
|
+
defaultModel: req.ctx.tenant.config.defaultModel ?? null,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Tool catalog for the current tenant. Used by the worker-agents
|
|
49
|
+
* settings page to render an allow-list picker instead of the
|
|
50
|
+
* old comma-separated freetext field.
|
|
51
|
+
*
|
|
52
|
+
* Returns ALL tools the host registry knows about (host built-ins
|
|
53
|
+
* + every active plugin's contributions). Per-agent allow-list
|
|
54
|
+
* filtering happens at the worker; this endpoint is just the
|
|
55
|
+
* universe to pick from.
|
|
56
|
+
*/
|
|
57
|
+
app.get("/api/tools", (req, res) => {
|
|
58
|
+
if (!req.ctx) {
|
|
59
|
+
res.status(500).json({ error: "no_ctx" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const entries = pluginRegistry.toolsForTenant(req.ctx.tenant.tenantId);
|
|
63
|
+
// De-dupe by tool name; if two plugins shipped the same name we
|
|
64
|
+
// still only show it once. Stable sort by name for the UI.
|
|
65
|
+
const byName = new Map();
|
|
66
|
+
for (const { pluginId, tool } of entries) {
|
|
67
|
+
if (byName.has(tool.schema.name))
|
|
68
|
+
continue;
|
|
69
|
+
byName.set(tool.schema.name, {
|
|
70
|
+
name: tool.schema.name,
|
|
71
|
+
description: tool.schema.description ?? "",
|
|
72
|
+
pluginId,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const tools = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
76
|
+
res.json({ tools });
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Skill catalog for the current tenant. Same role as /api/tools \u2014
|
|
80
|
+
* the universe of skills available (host-shipped + plugin-shipped)
|
|
81
|
+
* for the worker-agents allow-list picker.
|
|
82
|
+
*/
|
|
83
|
+
app.get("/api/skills", (req, res) => {
|
|
84
|
+
if (!req.ctx) {
|
|
85
|
+
res.status(500).json({ error: "no_ctx" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const skills = pluginRegistry.skillsForTenant(req.ctx.tenant.tenantId);
|
|
89
|
+
// Same shape as /api/tools \u2014 just the bits the picker UI needs.
|
|
90
|
+
// Description is the SKILL.md frontmatter so the picker can render
|
|
91
|
+
// a tooltip; the body markdown stays server-side.
|
|
92
|
+
const out = skills
|
|
93
|
+
.map((s) => ({
|
|
94
|
+
name: s.name,
|
|
95
|
+
description: s.description,
|
|
96
|
+
pluginId: s.source.pluginId,
|
|
97
|
+
// Surface frontmatter `scope:` so the worker-agents page can
|
|
98
|
+
// hide \"scope: main\" skills from a worker's effective list.
|
|
99
|
+
// Undefined = visible to both.
|
|
100
|
+
scope: s.scope,
|
|
101
|
+
}))
|
|
102
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
103
|
+
res.json({ skills: out });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=routes-core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes-core.js","sourceRoot":"","sources":["../../src/boot/routes-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Mount the static web dist + SPA fallback when configured. Returns
|
|
4
|
+
* true if hosting was activated, false in dev mode / on failure (so
|
|
5
|
+
* the caller can log appropriately).
|
|
6
|
+
*/
|
|
7
|
+
export declare function mountStaticSpa(app: Express): Promise<boolean>;
|
|
8
|
+
/** Whether this process is the one hosting the SPA. Cheap probe,
|
|
9
|
+
* same check `mountStaticSpa` uses internally; callers that need
|
|
10
|
+
* to publish the URL out-of-band consult this. */
|
|
11
|
+
export declare function isSpaHosted(): boolean;
|
|
12
|
+
//# sourceMappingURL=static-spa.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-spa.d.ts","sourceRoot":"","sources":["../../src/boot/static-spa.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA6CnE;AAED;;mDAEmD;AACnD,wBAAgB,WAAW,IAAI,OAAO,CAIrC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Optional in-process static SPA hosting.
|
|
2
|
+
//
|
|
3
|
+
// Two deployment modes:
|
|
4
|
+
//
|
|
5
|
+
// Dev (`npm run dev` from a checkout): vite hosts the web on its
|
|
6
|
+
// own port (5183 by default). TIANSHU_WEB_DIST is unset, this
|
|
7
|
+
// module is a no-op.
|
|
8
|
+
//
|
|
9
|
+
// Production / global install: the wizard's launchd plist sets
|
|
10
|
+
// TIANSHU_WEB_DIST to the bundled web dist directory and the
|
|
11
|
+
// server hosts the static files itself \u2014 the user only needs
|
|
12
|
+
// one port (3110) instead of two processes.
|
|
13
|
+
//
|
|
14
|
+
// Mount AFTER every `/api/*` and `/ws` handler so the SPA fallback
|
|
15
|
+
// catch-all only fires for non-API requests. The fallback (any
|
|
16
|
+
// unknown path \u2192 index.html) is what makes
|
|
17
|
+
// `/tenants/foo/users/bar/` work without a real route on the
|
|
18
|
+
// filesystem.
|
|
19
|
+
//
|
|
20
|
+
// Why we read index.html into a buffer rather than using
|
|
21
|
+
// `res.sendFile()`: under Express 5 + Node 22+'s send module,
|
|
22
|
+
// sendFile() with an absolute path consistently 404'd on our setup
|
|
23
|
+
// even though `existsSync(file)` returned true. We don't fully
|
|
24
|
+
// understand the resolution path send takes; bypassing it with a
|
|
25
|
+
// direct buffer write is simple and works the same on every Node
|
|
26
|
+
// version we test.
|
|
27
|
+
import express from "express";
|
|
28
|
+
/**
|
|
29
|
+
* Mount the static web dist + SPA fallback when configured. Returns
|
|
30
|
+
* true if hosting was activated, false in dev mode / on failure (so
|
|
31
|
+
* the caller can log appropriately).
|
|
32
|
+
*/
|
|
33
|
+
export async function mountStaticSpa(app) {
|
|
34
|
+
const webDistRaw = process.env.TIANSHU_WEB_DIST;
|
|
35
|
+
if (!webDistRaw || webDistRaw.length === 0)
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
const path = await import("node:path");
|
|
39
|
+
const fs = await import("node:fs");
|
|
40
|
+
const webDist = path.resolve(webDistRaw);
|
|
41
|
+
if (!fs.existsSync(path.join(webDist, "index.html"))) {
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.warn(`[tianshu] TIANSHU_WEB_DIST=${webDist} but no index.html there; ` +
|
|
44
|
+
"skipping static UI mount.");
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// Two-layer handler:
|
|
48
|
+
// 1. express.static handles `/index.html`, `/assets/*`, etc.
|
|
49
|
+
// fallthrough: true so requests it doesn't recognise
|
|
50
|
+
// cascade to the next middleware.
|
|
51
|
+
// 2. SPA fallback: any GET request that wasn't /api or /ws
|
|
52
|
+
// and wasn't a static asset \u2192 serve the pre-read
|
|
53
|
+
// index.html bytes. The React router on the client decides
|
|
54
|
+
// what to render.
|
|
55
|
+
app.use(express.static(webDist, { index: false, fallthrough: true }));
|
|
56
|
+
const indexHtml = fs.readFileSync(path.join(webDist, "index.html"));
|
|
57
|
+
app.use((req, res, next) => {
|
|
58
|
+
if (req.path.startsWith("/api/"))
|
|
59
|
+
return next();
|
|
60
|
+
if (req.path === "/api")
|
|
61
|
+
return next();
|
|
62
|
+
if (req.path.startsWith("/ws"))
|
|
63
|
+
return next();
|
|
64
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
65
|
+
return next();
|
|
66
|
+
// Anything else \u2014 /, /tenants/x/users/y/, /admin/foo \u2014 gets
|
|
67
|
+
// index.html. The SPA's router handles it.
|
|
68
|
+
res.type("html").send(indexHtml);
|
|
69
|
+
});
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.log(`[tianshu] serving web UI from ${webDist}`);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// eslint-disable-next-line no-console
|
|
76
|
+
console.warn(`[tianshu] failed to mount static web dist: ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Whether this process is the one hosting the SPA. Cheap probe,
|
|
81
|
+
* same check `mountStaticSpa` uses internally; callers that need
|
|
82
|
+
* to publish the URL out-of-band consult this. */
|
|
83
|
+
export function isSpaHosted() {
|
|
84
|
+
return Boolean(process.env.TIANSHU_WEB_DIST && process.env.TIANSHU_WEB_DIST.length > 0);
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=static-spa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-spa.js","sourceRoot":"","sources":["../../src/boot/static-spa.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,EAAE;AACF,wBAAwB;AACxB,EAAE;AACF,mEAAmE;AACnE,gEAAgE;AAChE,uBAAuB;AACvB,EAAE;AACF,iEAAiE;AACjE,+DAA+D;AAC/D,oEAAoE;AACpE,8CAA8C;AAC9C,EAAE;AACF,mEAAmE;AACnE,+DAA+D;AAC/D,gDAAgD;AAChD,6DAA6D;AAC7D,cAAc;AACd,EAAE;AACF,yDAAyD;AACzD,8DAA8D;AAC9D,mEAAmE;AACnE,+DAA+D;AAC/D,iEAAiE;AACjE,iEAAiE;AACjE,mBAAmB;AAGnB,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAY;IAC/C,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAChD,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEzD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;QACvC,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACzC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;YACrD,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,8BAA8B,OAAO,4BAA4B;gBAC/D,2BAA2B,CAC9B,CAAC;YACF,OAAO,KAAK,CAAC;QACf,CAAC;QACD,qBAAqB;QACrB,+DAA+D;QAC/D,0DAA0D;QAC1D,uCAAuC;QACvC,6DAA6D;QAC7D,2DAA2D;QAC3D,gEAAgE;QAChE,uBAAuB;QACvB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACtE,MAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;QACpE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACzB,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC;YAChD,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM;gBAAE,OAAO,IAAI,EAAE,CAAC;YACvC,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,EAAE,CAAC;YAC9C,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;gBAAE,OAAO,IAAI,EAAE,CAAC;YACjE,sEAAsE;YACtE,2CAA2C;YAC3C,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QACH,sCAAsC;QACtC,OAAO,CAAC,GAAG,CAAC,iCAAiC,OAAO,EAAE,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,sCAAsC;QACtC,OAAO,CAAC,IAAI,CACV,8CAA8C,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACjG,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;mDAEmD;AACnD,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CACZ,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,CACxE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Server as HttpServer } from "node:http";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
import type { GlobalOps } from "../core/global-ops.js";
|
|
4
|
+
import type { PluginRegistry } from "../core/plugins/registry.js";
|
|
5
|
+
export interface InstallChatWebSocketDeps {
|
|
6
|
+
server: HttpServer;
|
|
7
|
+
globalOps: GlobalOps;
|
|
8
|
+
pluginRegistry: PluginRegistry;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Install the `/ws` WebSocketServer on the provided HTTP server.
|
|
12
|
+
* Returns the WebSocketServer for the caller to keep alive / close
|
|
13
|
+
* on shutdown.
|
|
14
|
+
*/
|
|
15
|
+
export declare function installChatWebSocket(deps: InstallChatWebSocketDeps): WebSocketServer;
|
|
16
|
+
//# sourceMappingURL=ws-upgrade.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-upgrade.d.ts","sourceRoot":"","sources":["../../src/boot/ws-upgrade.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAOrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAGlE,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,EAAE,cAAc,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,wBAAwB,GAC7B,eAAe,CAyGjB"}
|