@templmf/temp-solf-lmf 0.0.163 → 0.0.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,269 +0,0 @@
1
- import "dotenv/config"; // 必须放在最前面:opencodeClient.js 在被 import 时就会读取 process.env
2
- import express from "express";
3
- import cookieParser from "cookie-parser";
4
- import crypto from "node:crypto";
5
- import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { opencode, opencodeBaseUrl, opencodeAuthHeaders } from "./opencodeClient.js";
8
- import { ownership } from "./ownership.js";
9
-
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
- const PORT = process.env.PORT || 3000;
12
- const COOKIE_NAME = "oc_uid";
13
-
14
- const app = express();
15
- app.use(express.json());
16
- app.use(cookieParser(process.env.COOKIE_SECRET || "dev-secret"));
17
- app.use(express.static(path.join(__dirname, "..", "public")));
18
-
19
- /* ---------------------------------------------------------------------- *
20
- * 1. 用户识别
21
- * 这是一个演示,用最简单的方式给每个浏览器发一个匿名用户 id(签名 cookie)。
22
- * 真实项目里把这一步换成你现有的登录体系即可,
23
- * 后面所有隔离逻辑只依赖 req.userId 这一个字段。
24
- * ---------------------------------------------------------------------- */
25
- app.use((req, res, next) => {
26
- let uid = req.signedCookies[COOKIE_NAME];
27
- if (!uid) {
28
- uid = crypto.randomUUID();
29
- res.cookie(COOKIE_NAME, uid, {
30
- httpOnly: true,
31
- signed: true,
32
- sameSite: "lax",
33
- maxAge: 1000 * 60 * 60 * 24 * 30,
34
- });
35
- }
36
- req.userId = uid;
37
- next();
38
- });
39
-
40
- app.get("/api/me", (req, res) => {
41
- res.json({ userId: req.userId, opencodeServer: opencodeBaseUrl });
42
- });
43
-
44
- // 可用的 provider / model 列表——这是全局配置,不涉及用户隔离,所有用户共享同一份
45
- app.get("/api/providers", async (req, res) => {
46
- try {
47
- const { providers, default: defaults } = await opencode.config.providers();
48
- res.json({ providers, defaults });
49
- } catch (err) {
50
- console.error(err);
51
- res.status(500).json({ error: "获取模型列表失败" });
52
- }
53
- });
54
-
55
- /* ---------------------------------------------------------------------- *
56
- * 2. session 归属校验中间件
57
- * 凡是带 :id 的路由,先确认这个 opencode sessionId 是不是当前用户创建的,
58
- * 不是的话直接 403 —— 这就是"同一个 opencode server,session 互不可见"的
59
- * 真正落地位置。opencode server 自己没有用户概念,隔离必须在网关层做。
60
- * ---------------------------------------------------------------------- */
61
- function requireOwnedSession(req, res, next) {
62
- const { id } = req.params;
63
- if (!ownership.owns(req.userId, id)) {
64
- return res.status(403).json({ error: "无权访问该 session(不属于当前用户)" });
65
- }
66
- next();
67
- }
68
-
69
- /* ---------------------------------------------------------------------- *
70
- * 3. Session 相关接口
71
- * ---------------------------------------------------------------------- */
72
-
73
- // 列出当前用户自己的 session(不是 opencode server 上的全部 session)
74
- app.get("/api/sessions", async (req, res) => {
75
- try {
76
- const ownedIds = new Set(ownership.listOwnedIds(req.userId));
77
- if (ownedIds.size === 0) return res.json([]);
78
-
79
- const all = await opencode.session.list();
80
- const mine = all.filter((s) => ownedIds.has(s.id));
81
- res.json(mine);
82
- } catch (err) {
83
- console.error(err);
84
- res.status(500).json({ error: "获取 session 列表失败" });
85
- }
86
- });
87
-
88
- // 新建 session,并把归属记录在网关这一侧
89
- app.post("/api/sessions", async (req, res) => {
90
- try {
91
- const title = req.body?.title || "新会话";
92
- const session = await opencode.session.create({ body: { title } });
93
- ownership.claim(req.userId, session.id);
94
- res.status(201).json(session);
95
- } catch (err) {
96
- console.error(err);
97
- res.status(500).json({ error: "创建 session 失败" });
98
- }
99
- });
100
-
101
- // 查看某个 session 的历史消息
102
- app.get("/api/sessions/:id/messages", requireOwnedSession, async (req, res) => {
103
- try {
104
- const messages = await opencode.session.messages({ path: { id: req.params.id } });
105
- res.json(messages);
106
- } catch (err) {
107
- console.error(err);
108
- res.status(500).json({ error: "获取消息失败" });
109
- }
110
- });
111
-
112
- // 发送一条 prompt,等待 opencode 的完整回复
113
- app.post("/api/sessions/:id/prompt", requireOwnedSession, async (req, res) => {
114
- try {
115
- const { text, providerID, modelID } = req.body;
116
- const body = { parts: [{ type: "text", text }] };
117
- if (providerID && modelID) body.model = { providerID, modelID };
118
-
119
- const reply = await opencode.session.prompt({
120
- path: { id: req.params.id },
121
- body,
122
- });
123
- res.json(reply);
124
- } catch (err) {
125
- console.error(err);
126
- res.status(500).json({ error: "发送消息失败" });
127
- }
128
- });
129
-
130
- // 中止正在进行的生成(对应 opencode SDK 的 session.abort)
131
- app.post("/api/sessions/:id/abort", requireOwnedSession, async (req, res) => {
132
- try {
133
- const ok = await opencode.session.abort({ path: { id: req.params.id } });
134
- res.json({ ok });
135
- } catch (err) {
136
- console.error(err);
137
- res.status(500).json({ error: "中止会话失败" });
138
- }
139
- });
140
-
141
- // 删除 session
142
- app.delete("/api/sessions/:id", requireOwnedSession, async (req, res) => {
143
- try {
144
- await opencode.session.delete({ path: { id: req.params.id } });
145
- ownership.release(req.userId, req.params.id);
146
- res.json({ ok: true });
147
- } catch (err) {
148
- console.error(err);
149
- res.status(500).json({ error: "删除 session 失败" });
150
- }
151
- });
152
-
153
- /* ---------------------------------------------------------------------- *
154
- * 4. 事件流(SSE)
155
- * opencode server 只对外暴露一条"全局"事件流,里面混着所有用户、所有
156
- * session 的事件。网关订阅这一条全局流一次,然后按 sessionId 找到归属
157
- * 用户,只广播给对应用户自己的浏览器连接,做到互不可见。
158
- * ---------------------------------------------------------------------- */
159
-
160
- /** userId -> Set<res>,同一个用户可能开多个标签页/多条连接 */
161
- const userConnections = new Map();
162
-
163
- function sendToUser(userId, event) {
164
- const conns = userConnections.get(userId);
165
- if (!conns) return;
166
- const payload = `data: ${JSON.stringify(event)}\n\n`;
167
- for (const res of conns) res.write(payload);
168
- }
169
-
170
- /** 从 opencode 事件里尽量取出 sessionId(不同事件类型字段名可能不同,按需扩展) */
171
- function extractSessionId(event) {
172
- const p = event.properties || {};
173
- return (
174
- p.sessionID ||
175
- p.sessionId ||
176
- p.info?.sessionID ||
177
- p.part?.sessionID ||
178
- p.session?.id ||
179
- null
180
- );
181
- }
182
-
183
- async function startGlobalEventBridge() {
184
- const debug = true; // 排查阶段先常开,确认没问题后可以改成按需开关
185
-
186
- while (true) {
187
- try {
188
- const res = await fetch(`${opencodeBaseUrl}/event`, {
189
- headers: { ...opencodeAuthHeaders },
190
- });
191
- if (!res.ok || !res.body) {
192
- throw new Error(`订阅事件流失败: HTTP ${res.status}`);
193
- }
194
- console.log("[opencode] 已连接全局事件流(手写 SSE 解析)");
195
-
196
- const reader = res.body.getReader();
197
- const decoder = new TextDecoder();
198
- let buffer = "";
199
-
200
- while (true) {
201
- const { value, done } = await reader.read();
202
- if (done) break;
203
- buffer += decoder.decode(value, { stream: true });
204
-
205
- // SSE 帧以空行(\n\n)分隔,一帧里可能有多行,我们只关心 "data:" 那一行
206
- let sepIndex;
207
- while ((sepIndex = buffer.indexOf("\n\n")) !== -1) {
208
- const frame = buffer.slice(0, sepIndex);
209
- buffer = buffer.slice(sepIndex + 2);
210
-
211
- const dataLine = frame.split("\n").find((l) => l.startsWith("data:"));
212
- if (!dataLine) continue;
213
- const jsonText = dataLine.slice(5).trim();
214
- if (!jsonText) continue;
215
-
216
- let event;
217
- try {
218
- event = JSON.parse(jsonText);
219
- } catch {
220
- if (debug) console.log(`[opencode:raw] 无法解析为 JSON: ${jsonText}`);
221
- continue;
222
- }
223
-
224
- if (debug) console.log(`[opencode:raw] ${jsonText}`);
225
-
226
- const sessionId = extractSessionId(event);
227
- if (!sessionId) continue; // 非 session 相关事件(如心跳)忽略
228
- const ownerId = ownership.findOwner(sessionId);
229
- if (debug) {
230
- console.log(
231
- `[opencode:route] session=${sessionId} owner=${ownerId ?? "(无归属记录,事件将被丢弃)"}`
232
- );
233
- }
234
- if (ownerId) sendToUser(ownerId, event);
235
- }
236
- }
237
- console.warn("[opencode] 事件流被服务端关闭,3 秒后重连");
238
- } catch (err) {
239
- console.error("[opencode] 事件流出错,3 秒后重连:", err.message);
240
- }
241
- await new Promise((r) => setTimeout(r, 3000));
242
- }
243
- }
244
- startGlobalEventBridge();
245
-
246
- app.get("/api/events", (req, res) => {
247
- req.socket.setNoDelay(true); // 关键:SSE 都是频繁的小包,Nagle 算法会把它们攒在一起延迟发送
248
- res.set({
249
- "Content-Type": "text/event-stream",
250
- "Cache-Control": "no-cache",
251
- Connection: "keep-alive",
252
- });
253
- res.flushHeaders();
254
-
255
- const userId = req.userId;
256
- if (!userConnections.has(userId)) userConnections.set(userId, new Set());
257
- userConnections.get(userId).add(res);
258
-
259
- res.write(`data: ${JSON.stringify({ type: "connected" })}\n\n`);
260
-
261
- req.on("close", () => {
262
- userConnections.get(userId)?.delete(res);
263
- });
264
- });
265
-
266
- app.listen(PORT, () => {
267
- console.log(`网关已启动: http://localhost:${PORT}`);
268
- console.log(`共用的 opencode server: ${opencodeBaseUrl}`);
269
- });
@@ -1,40 +0,0 @@
1
- import { createOpencodeClient } from "@opencode-ai/sdk";
2
-
3
- const baseUrl = process.env.OPENCODE_SERVER_URL || "http://127.0.0.1:4096";
4
- const username = process.env.OPENCODE_SERVER_USERNAME;
5
- const password = process.env.OPENCODE_SERVER_PASSWORD;
6
-
7
- // 整个网关进程只建立一个到 opencode server 的连接。
8
- // 多用户共享这一个连接,隔离逻辑完全在网关层(ownership store)里做,
9
- // opencode server 本身并不知道"用户"这个概念。
10
- const fetchOptions = {};
11
- if (password) {
12
- const basic = Buffer.from(`${username || "opencode"}:${password}`).toString("base64");
13
- fetchOptions.headers = { Authorization: `Basic ${basic}` };
14
- }
15
-
16
- export const opencode = createOpencodeClient({
17
- baseUrl,
18
- // data 模式:方法直接返回数据本身,出错时抛异常,方便 try/catch
19
- responseStyle: "data",
20
- throwOnError: true,
21
- fetch: async (input, init) => {
22
- const res = await fetch(input, {
23
- ...init,
24
- headers: { ...(init && init.headers), ...fetchOptions.headers },
25
- });
26
- if (!res.ok) {
27
- // 克隆一份用于打印,避免 body 被消费掉导致 SDK 那边读不到
28
- const cloned = res.clone();
29
- const text = await cloned.text().catch(() => "<无法读取 body>");
30
- console.error(
31
- `[opencode] ${init?.method || "GET"} ${input} -> ${res.status} ${res.statusText}\n` +
32
- `响应体: ${text}`
33
- );
34
- }
35
- return res;
36
- },
37
- });
38
-
39
- export const opencodeAuthHeaders = fetchOptions.headers || {};
40
- export { baseUrl as opencodeBaseUrl };
@@ -1,60 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
- const DATA_DIR = path.join(__dirname, "..", "data");
7
- const DATA_FILE = path.join(DATA_DIR, "ownership.json");
8
-
9
- if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
10
- if (!fs.existsSync(DATA_FILE)) fs.writeFileSync(DATA_FILE, "{}");
11
-
12
- /** @type {Record<string, {sessionIds: string[], name?: string}>} */
13
- let db = JSON.parse(fs.readFileSync(DATA_FILE, "utf-8"));
14
-
15
- function persist() {
16
- fs.writeFileSync(DATA_FILE, JSON.stringify(db, null, 2));
17
- }
18
-
19
- function ensureUser(userId) {
20
- if (!db[userId]) db[userId] = { sessionIds: [] };
21
- return db[userId];
22
- }
23
-
24
- export const ownership = {
25
- /** 记录:某个 opencode sessionId 属于某个 userId */
26
- claim(userId, sessionId) {
27
- const u = ensureUser(userId);
28
- if (!u.sessionIds.includes(sessionId)) {
29
- u.sessionIds.push(sessionId);
30
- persist();
31
- }
32
- },
33
-
34
- /** 判断某个 session 是否属于该用户 —— 所有跨 session 的访问都要过这一关 */
35
- owns(userId, sessionId) {
36
- const u = db[userId];
37
- return !!u && u.sessionIds.includes(sessionId);
38
- },
39
-
40
- /** 该用户拥有的全部 sessionId */
41
- listOwnedIds(userId) {
42
- return db[userId]?.sessionIds ?? [];
43
- },
44
-
45
- /** session 删除后同步移除归属记录 */
46
- release(userId, sessionId) {
47
- const u = db[userId];
48
- if (!u) return;
49
- u.sessionIds = u.sessionIds.filter((id) => id !== sessionId);
50
- persist();
51
- },
52
-
53
- /** 反查某个 sessionId 属于哪个 userId(SSE 广播过滤时要用) */
54
- findOwner(sessionId) {
55
- for (const [userId, u] of Object.entries(db)) {
56
- if (u.sessionIds.includes(sessionId)) return userId;
57
- }
58
- return null;
59
- },
60
- };