vibe-coding-master 0.2.6 → 0.2.7
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/README.md +147 -5
- package/dist/backend/api/gateway-routes.js +17 -0
- package/dist/backend/api/round-routes.js +1 -1
- package/dist/backend/gateway/channels/weixin-ilink-channel.js +304 -0
- package/dist/backend/gateway/gateway-audit-log.js +39 -0
- package/dist/backend/gateway/gateway-command-parser.js +77 -0
- package/dist/backend/gateway/gateway-service.js +848 -0
- package/dist/backend/gateway/gateway-settings-service.js +214 -0
- package/dist/backend/server.js +40 -2
- package/dist/backend/services/claude-hook-service.js +13 -4
- package/dist/backend/services/round-service.js +110 -64
- package/dist/backend/services/session-service.js +10 -4
- package/dist/backend/services/task-service.js +32 -3
- package/dist/backend/services/translation-service.js +15 -0
- package/dist/shared/types/gateway.js +1 -0
- package/dist-frontend/assets/{index-CPXFnxAY.css → index-7lq6YPCq.css} +1 -1
- package/dist-frontend/assets/index-DHuS-DYr.js +92 -0
- package/dist-frontend/index.html +2 -2
- package/docs/gateway-design.md +200 -27
- package/docs/product-design.md +34 -13
- package/docs/v0.2-implementation-plan.md +22 -7
- package/package.json +2 -1
- package/dist-frontend/assets/index-D0_02lmQ.js +0 -90
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
4
|
+
const MAX_DEDUPE_IDS = 1000;
|
|
5
|
+
export function createGatewaySettingsService(deps) {
|
|
6
|
+
const settingsPath = deps.settingsPath ?? path.join(homedir(), ".vcm", "gateway", "settings.json");
|
|
7
|
+
const auditPath = deps.auditPath ?? path.join(homedir(), ".vcm", "gateway", "audit.jsonl");
|
|
8
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
9
|
+
let cachedSettings = null;
|
|
10
|
+
async function loadSettings() {
|
|
11
|
+
if (cachedSettings) {
|
|
12
|
+
return cachedSettings;
|
|
13
|
+
}
|
|
14
|
+
if (!(await deps.fs.pathExists(settingsPath))) {
|
|
15
|
+
cachedSettings = normalizeSettings({}, now());
|
|
16
|
+
await saveSettings(cachedSettings);
|
|
17
|
+
return cachedSettings;
|
|
18
|
+
}
|
|
19
|
+
cachedSettings = normalizeSettings(await deps.fs.readJson(settingsPath), now());
|
|
20
|
+
return cachedSettings;
|
|
21
|
+
}
|
|
22
|
+
async function saveSettings(settings) {
|
|
23
|
+
cachedSettings = normalizeSettings(settings, now());
|
|
24
|
+
await deps.fs.writeJsonAtomic(settingsPath, cachedSettings);
|
|
25
|
+
return cachedSettings;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
loadSettings,
|
|
29
|
+
async updateSettings(input) {
|
|
30
|
+
const current = await loadSettings();
|
|
31
|
+
return saveSettings({
|
|
32
|
+
...current,
|
|
33
|
+
enabled: input.enabled ?? current.enabled,
|
|
34
|
+
translationEnabled: input.translationEnabled ?? current.translationEnabled,
|
|
35
|
+
currentProjectId: input.currentProjectId !== undefined ? normalizeNullableString(input.currentProjectId) : current.currentProjectId,
|
|
36
|
+
currentTaskSlug: input.currentTaskSlug !== undefined ? normalizeNullableString(input.currentTaskSlug) : current.currentTaskSlug,
|
|
37
|
+
updatedAt: now()
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
saveSettings,
|
|
41
|
+
async resetBinding() {
|
|
42
|
+
const current = await loadSettings();
|
|
43
|
+
return saveSettings({
|
|
44
|
+
...current,
|
|
45
|
+
enabled: false,
|
|
46
|
+
binding: createDefaultBinding(),
|
|
47
|
+
dedupe: {
|
|
48
|
+
recentInboundMessageIds: []
|
|
49
|
+
},
|
|
50
|
+
pushCursors: {},
|
|
51
|
+
lastPollStatus: {
|
|
52
|
+
state: "idle"
|
|
53
|
+
},
|
|
54
|
+
lastMessageStatus: null,
|
|
55
|
+
updatedAt: now()
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
expose(settings, running = false) {
|
|
59
|
+
return {
|
|
60
|
+
version: 1,
|
|
61
|
+
enabled: settings.enabled,
|
|
62
|
+
running,
|
|
63
|
+
channel: settings.channel,
|
|
64
|
+
translationEnabled: settings.translationEnabled,
|
|
65
|
+
currentProjectId: settings.currentProjectId,
|
|
66
|
+
currentTaskSlug: settings.currentTaskSlug,
|
|
67
|
+
binding: {
|
|
68
|
+
accountId: settings.binding.accountId,
|
|
69
|
+
baseUrl: settings.binding.baseUrl,
|
|
70
|
+
boundUserId: settings.binding.boundUserId,
|
|
71
|
+
loginUserId: settings.binding.loginUserId,
|
|
72
|
+
tokenConfigured: Boolean(settings.binding.token)
|
|
73
|
+
},
|
|
74
|
+
pendingConfirmations: settings.pendingConfirmations,
|
|
75
|
+
lastPollStatus: settings.lastPollStatus,
|
|
76
|
+
lastMessageStatus: settings.lastMessageStatus,
|
|
77
|
+
updatedAt: settings.updatedAt
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
getSettingsPath() {
|
|
81
|
+
return settingsPath;
|
|
82
|
+
},
|
|
83
|
+
getAuditPath() {
|
|
84
|
+
return auditPath;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function normalizeSettings(input, timestamp) {
|
|
89
|
+
const bindingInput = isObject(input.binding) ? input.binding : {};
|
|
90
|
+
const dedupeInput = isObject(input.dedupe) ? input.dedupe : {};
|
|
91
|
+
const pendingInput = isObject(input.pendingConfirmations)
|
|
92
|
+
? input.pendingConfirmations
|
|
93
|
+
: {};
|
|
94
|
+
const pushCursors = isObject(input.pushCursors) ? input.pushCursors : {};
|
|
95
|
+
return {
|
|
96
|
+
version: 1,
|
|
97
|
+
enabled: input.enabled === true,
|
|
98
|
+
channel: "weixin-ilink",
|
|
99
|
+
translationEnabled: input.translationEnabled !== false,
|
|
100
|
+
currentProjectId: normalizeNullableString(input.currentProjectId),
|
|
101
|
+
currentTaskSlug: normalizeNullableString(input.currentTaskSlug),
|
|
102
|
+
binding: {
|
|
103
|
+
...createDefaultBinding(),
|
|
104
|
+
accountId: normalizeNullableString(bindingInput.accountId),
|
|
105
|
+
baseUrl: normalizeBaseUrl(bindingInput.baseUrl),
|
|
106
|
+
boundUserId: normalizeNullableString(bindingInput.boundUserId),
|
|
107
|
+
loginUserId: normalizeNullableString(bindingInput.loginUserId),
|
|
108
|
+
token: normalizeNullableString(bindingInput.token),
|
|
109
|
+
getUpdatesBuf: typeof bindingInput.getUpdatesBuf === "string" ? bindingInput.getUpdatesBuf : "",
|
|
110
|
+
contextTokens: isObject(bindingInput.contextTokens)
|
|
111
|
+
? normalizeStringRecord(bindingInput.contextTokens)
|
|
112
|
+
: {}
|
|
113
|
+
},
|
|
114
|
+
dedupe: {
|
|
115
|
+
recentInboundMessageIds: Array.isArray(dedupeInput.recentInboundMessageIds)
|
|
116
|
+
? dedupeInput.recentInboundMessageIds.filter((value) => typeof value === "string").slice(-MAX_DEDUPE_IDS)
|
|
117
|
+
: []
|
|
118
|
+
},
|
|
119
|
+
pendingConfirmations: {
|
|
120
|
+
closeTask: normalizeCloseTaskConfirmation(pendingInput.closeTask)
|
|
121
|
+
},
|
|
122
|
+
pushCursors: normalizePushCursors(pushCursors),
|
|
123
|
+
lastPollStatus: normalizePollStatus(input.lastPollStatus),
|
|
124
|
+
lastMessageStatus: normalizeMessageStatus(input.lastMessageStatus),
|
|
125
|
+
updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : timestamp
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function createDefaultBinding() {
|
|
129
|
+
return {
|
|
130
|
+
accountId: null,
|
|
131
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
132
|
+
boundUserId: null,
|
|
133
|
+
loginUserId: null,
|
|
134
|
+
token: null,
|
|
135
|
+
getUpdatesBuf: "",
|
|
136
|
+
contextTokens: {}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function normalizeCloseTaskConfirmation(input) {
|
|
140
|
+
if (!isObject(input)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const taskSlug = normalizeNullableString(input.taskSlug);
|
|
144
|
+
const createdAt = normalizeNullableString(input.createdAt);
|
|
145
|
+
const expiresAt = normalizeNullableString(input.expiresAt);
|
|
146
|
+
if (!taskSlug || !createdAt || !expiresAt) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return { taskSlug, createdAt, expiresAt };
|
|
150
|
+
}
|
|
151
|
+
function normalizePushCursors(input) {
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [key, value] of Object.entries(input)) {
|
|
154
|
+
if (!isObject(value)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
out[key] = {
|
|
158
|
+
lastTranscriptEventId: normalizeNullableString(value.lastTranscriptEventId),
|
|
159
|
+
lastTranscriptTimestamp: normalizeNullableString(value.lastTranscriptTimestamp)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
function normalizePollStatus(input) {
|
|
165
|
+
if (!isObject(input)) {
|
|
166
|
+
return { state: "idle" };
|
|
167
|
+
}
|
|
168
|
+
const state = input.state === "running" || input.state === "error" || input.state === "expired"
|
|
169
|
+
? input.state
|
|
170
|
+
: "idle";
|
|
171
|
+
return {
|
|
172
|
+
state,
|
|
173
|
+
checkedAt: normalizeNullableString(input.checkedAt) ?? undefined,
|
|
174
|
+
error: normalizeNullableString(input.error) ?? undefined
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function normalizeMessageStatus(input) {
|
|
178
|
+
if (!isObject(input)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
checkedAt: normalizeNullableString(input.checkedAt) ?? undefined,
|
|
183
|
+
direction: input.direction === "inbound" || input.direction === "outbound" ? input.direction : undefined,
|
|
184
|
+
command: normalizeNullableString(input.command) ?? undefined,
|
|
185
|
+
result: input.result === "ok" || input.result === "ignored" || input.result === "error" ? input.result : undefined,
|
|
186
|
+
preview: normalizeNullableString(input.preview) ?? undefined,
|
|
187
|
+
error: normalizeNullableString(input.error) ?? undefined
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function normalizeBaseUrl(input) {
|
|
191
|
+
const raw = typeof input === "string" ? input.trim() : "";
|
|
192
|
+
if (!raw) {
|
|
193
|
+
return DEFAULT_BASE_URL;
|
|
194
|
+
}
|
|
195
|
+
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
|
196
|
+
return raw.replace(/\/+$/, "");
|
|
197
|
+
}
|
|
198
|
+
return `https://${raw.replace(/\/+$/, "")}`;
|
|
199
|
+
}
|
|
200
|
+
function normalizeNullableString(input) {
|
|
201
|
+
return typeof input === "string" && input.trim() ? input.trim() : null;
|
|
202
|
+
}
|
|
203
|
+
function normalizeStringRecord(input) {
|
|
204
|
+
const out = {};
|
|
205
|
+
for (const [key, value] of Object.entries(input)) {
|
|
206
|
+
if (typeof value === "string") {
|
|
207
|
+
out[key] = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function isObject(value) {
|
|
213
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
214
|
+
}
|
package/dist/backend/server.js
CHANGED
|
@@ -14,6 +14,11 @@ import { createHarnessService, createScriptFixedHarnessInstaller } from "./servi
|
|
|
14
14
|
import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
|
|
15
15
|
import { createNodePtyTerminalRuntime } from "./runtime/node-pty-runtime.js";
|
|
16
16
|
import { createOpenAiCompatibleTranslationProvider } from "./adapters/translation-provider.js";
|
|
17
|
+
import { registerGatewayRoutes } from "./api/gateway-routes.js";
|
|
18
|
+
import { createWeixinIlinkChannel } from "./gateway/channels/weixin-ilink-channel.js";
|
|
19
|
+
import { createGatewayAuditLog } from "./gateway/gateway-audit-log.js";
|
|
20
|
+
import { createGatewayService } from "./gateway/gateway-service.js";
|
|
21
|
+
import { createGatewaySettingsService } from "./gateway/gateway-settings-service.js";
|
|
17
22
|
import { createProjectService } from "./services/project-service.js";
|
|
18
23
|
import { createSessionRegistry } from "./runtime/session-registry.js";
|
|
19
24
|
import { createSessionService } from "./services/session-service.js";
|
|
@@ -90,7 +95,14 @@ export async function createServer(deps, options = {}) {
|
|
|
90
95
|
taskService: deps.taskService,
|
|
91
96
|
translationService: deps.translationService
|
|
92
97
|
});
|
|
98
|
+
registerGatewayRoutes(app, { gatewayService: deps.gatewayService });
|
|
93
99
|
registerTerminalWs(app, { runtime: deps.runtime });
|
|
100
|
+
app.addHook("onReady", async () => {
|
|
101
|
+
await deps.gatewayService.start();
|
|
102
|
+
});
|
|
103
|
+
app.addHook("onClose", async () => {
|
|
104
|
+
deps.gatewayService.stop();
|
|
105
|
+
});
|
|
94
106
|
if (options.staticDir) {
|
|
95
107
|
await app.register(fastifyStatic, {
|
|
96
108
|
root: options.staticDir,
|
|
@@ -162,7 +174,12 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
162
174
|
sessionService,
|
|
163
175
|
taskService
|
|
164
176
|
});
|
|
165
|
-
const roundService = createRoundService({
|
|
177
|
+
const roundService = createRoundService({
|
|
178
|
+
fs,
|
|
179
|
+
onSessionStatusChange: async ({ repoRoot, taskSlug, status }) => {
|
|
180
|
+
await taskService.updateTaskStatus(repoRoot, taskSlug, status);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
166
183
|
const transcripts = createClaudeTranscriptService();
|
|
167
184
|
const translationService = createTranslationService({
|
|
168
185
|
runtime,
|
|
@@ -174,6 +191,25 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
174
191
|
appSettings,
|
|
175
192
|
provider: createOpenAiCompatibleTranslationProvider()
|
|
176
193
|
});
|
|
194
|
+
const gatewaySettings = createGatewaySettingsService({ fs });
|
|
195
|
+
const gatewayAudit = createGatewayAuditLog({
|
|
196
|
+
fs,
|
|
197
|
+
auditPath: gatewaySettings.getAuditPath()
|
|
198
|
+
});
|
|
199
|
+
const gatewayService = createGatewayService({
|
|
200
|
+
fs,
|
|
201
|
+
settings: gatewaySettings,
|
|
202
|
+
audit: gatewayAudit,
|
|
203
|
+
channel: createWeixinIlinkChannel(),
|
|
204
|
+
projectService,
|
|
205
|
+
taskService,
|
|
206
|
+
sessionService,
|
|
207
|
+
messageService,
|
|
208
|
+
translationService,
|
|
209
|
+
roundService,
|
|
210
|
+
runtime,
|
|
211
|
+
appSettings
|
|
212
|
+
});
|
|
177
213
|
const claudeHookService = createClaudeHookService({
|
|
178
214
|
projectService,
|
|
179
215
|
taskService,
|
|
@@ -181,7 +217,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
181
217
|
messageService,
|
|
182
218
|
roundService,
|
|
183
219
|
translationService,
|
|
184
|
-
appSettings
|
|
220
|
+
appSettings,
|
|
221
|
+
gatewayService
|
|
185
222
|
});
|
|
186
223
|
return {
|
|
187
224
|
appSettings,
|
|
@@ -196,6 +233,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
196
233
|
roundService,
|
|
197
234
|
statusService,
|
|
198
235
|
translationService,
|
|
236
|
+
gatewayService,
|
|
199
237
|
runtime
|
|
200
238
|
};
|
|
201
239
|
}
|
|
@@ -43,6 +43,7 @@ export function createClaudeHookService(deps) {
|
|
|
43
43
|
cwd: stringOrUndefined(input.event.cwd)
|
|
44
44
|
});
|
|
45
45
|
await deps.roundService.recordClaudeHookEvent({
|
|
46
|
+
repoRoot: context.project.repoRoot,
|
|
46
47
|
stateRepoRoot: context.taskRepoRoot,
|
|
47
48
|
stateRoot: context.config.stateRoot,
|
|
48
49
|
taskSlug: input.taskSlug,
|
|
@@ -57,7 +58,7 @@ export function createClaudeHookService(deps) {
|
|
|
57
58
|
role: input.role,
|
|
58
59
|
sessionId: session.id,
|
|
59
60
|
boundaryKind: "start",
|
|
60
|
-
occurredAt: session.
|
|
61
|
+
occurredAt: session.lastTurnStartedAt ?? session.updatedAt
|
|
61
62
|
});
|
|
62
63
|
}
|
|
63
64
|
const submitted = await deps.messageService.confirmPromptSubmitted({
|
|
@@ -112,6 +113,7 @@ export function createClaudeHookService(deps) {
|
|
|
112
113
|
cwd: stringOrUndefined(input.event.cwd)
|
|
113
114
|
});
|
|
114
115
|
await deps.roundService.recordClaudeHookEvent({
|
|
116
|
+
repoRoot: context.project.repoRoot,
|
|
115
117
|
stateRepoRoot: context.taskRepoRoot,
|
|
116
118
|
stateRoot: context.config.stateRoot,
|
|
117
119
|
taskSlug: input.taskSlug,
|
|
@@ -120,12 +122,12 @@ export function createClaudeHookService(deps) {
|
|
|
120
122
|
settleGuard: async () => {
|
|
121
123
|
const pending = await deps.messageService.listPendingRouteFiles(settleRouteDispatchInput);
|
|
122
124
|
if (pending.length === 0) {
|
|
123
|
-
return { action: "
|
|
125
|
+
return { action: "stop" };
|
|
124
126
|
}
|
|
125
127
|
const retried = await deps.messageService.scanAndDispatchPendingRouteFiles(settleRouteDispatchInput);
|
|
126
128
|
return retried.some((result) => result.delivered)
|
|
127
129
|
? { action: "continue", reason: "pending route message dispatched" }
|
|
128
|
-
: { action: "
|
|
130
|
+
: { action: "stop" };
|
|
129
131
|
}
|
|
130
132
|
});
|
|
131
133
|
if (session) {
|
|
@@ -136,9 +138,16 @@ export function createClaudeHookService(deps) {
|
|
|
136
138
|
role: input.role,
|
|
137
139
|
sessionId: session.id,
|
|
138
140
|
boundaryKind: "end",
|
|
139
|
-
occurredAt: session.
|
|
141
|
+
occurredAt: session.lastTurnEndedAt ?? session.updatedAt
|
|
140
142
|
});
|
|
141
143
|
}
|
|
144
|
+
if (session && input.role === "project-manager") {
|
|
145
|
+
void deps.gatewayService?.handlePmStop({
|
|
146
|
+
repoRoot: context.project.repoRoot,
|
|
147
|
+
taskSlug: input.taskSlug,
|
|
148
|
+
session
|
|
149
|
+
}).catch(() => undefined);
|
|
150
|
+
}
|
|
142
151
|
const dispatched = await deps.messageService.scanAndDispatchPendingRouteFiles(scopedRouteDispatchInput);
|
|
143
152
|
return {
|
|
144
153
|
ok: true,
|