velaclaw-dev 0.2.0
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/.gitignore +14 -0
- package/ARCHITECTURE.md +143 -0
- package/README.dev.md +208 -0
- package/README.local-before-remote-sync.md +224 -0
- package/README.md +211 -0
- package/README.public.md +115 -0
- package/RELEASING.md +162 -0
- package/TESTING.md +195 -0
- package/dist/cli.js +213 -0
- package/dist/data.js +2988 -0
- package/dist/server.js +1020 -0
- package/dist/ui.js +1486 -0
- package/members/LAUNCH_CHECKLIST.md +13 -0
- package/members/README.md +17 -0
- package/members/member-template/README.md +9 -0
- package/members/member-template/private-docs/README.md +3 -0
- package/members/member-template/private-memory/README.md +3 -0
- package/members/member-template/private-skills/README.md +4 -0
- package/members/member-template/private-tools/README.md +4 -0
- package/members/member-template/runtime/config/README.md +3 -0
- package/members/member-template/runtime/config/local-plugins/member-quota-guard/index.js +123 -0
- package/members/member-template/runtime/config/local-plugins/member-quota-guard/openclaw.plugin.json +19 -0
- package/members/member-template/runtime/config/local-plugins/member-quota-guard/package.json +10 -0
- package/members/member-template/runtime/config/local-plugins/member-runtime-upgrader/index.js +97 -0
- package/members/member-template/runtime/config/local-plugins/member-runtime-upgrader/openclaw.plugin.json +21 -0
- package/members/member-template/runtime/config/local-plugins/member-runtime-upgrader/package.json +10 -0
- package/members/member-template/runtime/config/local-plugins/shared-asset-injector/index.js +548 -0
- package/members/member-template/runtime/config/local-plugins/shared-asset-injector/openclaw.plugin.json +33 -0
- package/members/member-template/runtime/config/local-plugins/shared-asset-injector/package.json +10 -0
- package/members/member-template/runtime/config/openclaw.json +104 -0
- package/members/member-template/runtime/docker-compose.yml +53 -0
- package/members/member-template/runtime/logs/README.md +3 -0
- package/members/member-template/runtime/secrets/.gitkeep +1 -0
- package/members/member-template/runtime/secrets/README.md +3 -0
- package/members/member-template/runtime/workspace/.gitkeep +1 -0
- package/members/member-template/runtime/workspace/README.md +3 -0
- package/package.json +57 -0
- package/pic/banner.jpg +0 -0
- package/provision-member.md +87 -0
- package/scripts/shared-asset-stack-test.mjs +369 -0
- package/scripts/shared-skill-combo-test.mjs +282 -0
- package/scripts/team-load-test.mjs +358 -0
- package/scripts/verify-install.mjs +44 -0
- package/services/litellm/config.yaml +35 -0
- package/services/litellm/docker-compose.yml +36 -0
- package/services/litellm/litellm.env.example +13 -0
- package/shared-snapshots/README.md +16 -0
- package/shared-snapshots/docs/README.md +3 -0
- package/shared-snapshots/memory/README.md +3 -0
- package/shared-snapshots/skills/README.md +3 -0
- package/shared-snapshots/tools/README.md +4 -0
- package/shared-snapshots/workflows/README.md +3 -0
- package/team-assets/README.md +11 -0
- package/team-assets/policies/README.md +7 -0
- package/team-assets/policies/asset-visibility.md +24 -0
- package/team-assets/policies/high-risk-action-approval.md +18 -0
- package/team-assets/policies/promotion-rules.md +25 -0
- package/team-assets/policies/tool-binding-rules.md +26 -0
- package/team-assets/shared-docs/README.md +3 -0
- package/team-assets/shared-memory/README.md +8 -0
- package/team-assets/shared-skills/README.md +8 -0
- package/team-assets/shared-tools/README.md +8 -0
- package/team-assets/shared-workflows/README.md +9 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: REPLACE_COMPOSE_PROJECT
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
openclaw-member:
|
|
5
|
+
image: ghcr.io/openclaw/openclaw:latest
|
|
6
|
+
container_name: REPLACE_CONTAINER_ID
|
|
7
|
+
restart: unless-stopped
|
|
8
|
+
ports:
|
|
9
|
+
- "127.0.0.1:18800:18789"
|
|
10
|
+
environment:
|
|
11
|
+
OPENCLAW_CONFIG_DIR: /home/node/.openclaw
|
|
12
|
+
OPENCLAW_WORKSPACE_DIR: /home/node/.openclaw/workspace
|
|
13
|
+
HTTP_PROXY: http://host.docker.internal:17892
|
|
14
|
+
HTTPS_PROXY: http://host.docker.internal:17892
|
|
15
|
+
http_proxy: http://host.docker.internal:17892
|
|
16
|
+
https_proxy: http://host.docker.internal:17892
|
|
17
|
+
NO_PROXY: 127.0.0.1,localhost,::1,host.docker.internal
|
|
18
|
+
no_proxy: 127.0.0.1,localhost,::1,host.docker.internal
|
|
19
|
+
user: "1000:1000"
|
|
20
|
+
extra_hosts:
|
|
21
|
+
- "host.docker.internal:host-gateway"
|
|
22
|
+
security_opt:
|
|
23
|
+
- no-new-privileges:true
|
|
24
|
+
cap_drop:
|
|
25
|
+
- ALL
|
|
26
|
+
read_only: true
|
|
27
|
+
tmpfs:
|
|
28
|
+
- /tmp:size=256m,mode=1777
|
|
29
|
+
pids_limit: 256
|
|
30
|
+
mem_limit: 2g
|
|
31
|
+
cpus: 1.5
|
|
32
|
+
volumes:
|
|
33
|
+
- ./config:/home/node/.openclaw:rw
|
|
34
|
+
- ./workspace:/home/node/.openclaw/workspace:rw
|
|
35
|
+
- ./secrets:/home/node/.openclaw/secrets:rw
|
|
36
|
+
- ../../private-memory:/home/node/.openclaw/workspace/private-memory:rw
|
|
37
|
+
- ../../private-skills:/home/node/.openclaw/workspace/private-skills:rw
|
|
38
|
+
- ../../private-tools:/home/node/.openclaw/workspace/private-tools:rw
|
|
39
|
+
- ../../private-docs:/home/node/.openclaw/workspace/private-docs:rw
|
|
40
|
+
- ../../../../teams/REPLACE_TEAM_SLUG/assets/current:/srv/team-shared:ro
|
|
41
|
+
- ../../../../teams/REPLACE_TEAM_SLUG/assets/current:/home/node/.openclaw/workspace/team-shared:ro
|
|
42
|
+
healthcheck:
|
|
43
|
+
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:18789/healthz"]
|
|
44
|
+
interval: 30s
|
|
45
|
+
timeout: 5s
|
|
46
|
+
retries: 5
|
|
47
|
+
start_period: 30s
|
|
48
|
+
networks:
|
|
49
|
+
- member_isolated
|
|
50
|
+
|
|
51
|
+
networks:
|
|
52
|
+
member_isolated:
|
|
53
|
+
driver: bridge
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "velaclaw-dev",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Velaclaw control-plane MVP prototype",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"velaclaw": "dist/cli.js",
|
|
9
|
+
"velaclaw-dev": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"scripts",
|
|
14
|
+
"members/member-template",
|
|
15
|
+
"members/README.md",
|
|
16
|
+
"members/LAUNCH_CHECKLIST.md",
|
|
17
|
+
"services/litellm/docker-compose.yml",
|
|
18
|
+
"services/litellm/config.yaml",
|
|
19
|
+
"services/litellm/litellm.env.example",
|
|
20
|
+
"team-assets",
|
|
21
|
+
"shared-snapshots",
|
|
22
|
+
"ARCHITECTURE.md",
|
|
23
|
+
"README.md",
|
|
24
|
+
"README.dev.md",
|
|
25
|
+
"README.public.md",
|
|
26
|
+
"TESTING.md",
|
|
27
|
+
"RELEASING.md",
|
|
28
|
+
"provision-member.md",
|
|
29
|
+
"pic",
|
|
30
|
+
".gitignore"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "tsx watch src/server.ts",
|
|
37
|
+
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"start": "node dist/server.js",
|
|
39
|
+
"load:test-team": "node scripts/team-load-test.mjs",
|
|
40
|
+
"smoke:skills": "node scripts/shared-skill-combo-test.mjs",
|
|
41
|
+
"smoke:assets": "node scripts/shared-asset-stack-test.mjs",
|
|
42
|
+
"verify:install": "node scripts/verify-install.mjs",
|
|
43
|
+
"release:check": "npm run build && npm run verify:install && npm pack --dry-run",
|
|
44
|
+
"publish:npm": "npm publish --access public",
|
|
45
|
+
"publish:private": "npm publish",
|
|
46
|
+
"prepack": "npm run build"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"express": "^4.21.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/express": "^5.0.1",
|
|
53
|
+
"@types/node": "^22.15.3",
|
|
54
|
+
"tsx": "^4.19.4",
|
|
55
|
+
"typescript": "^5.8.3"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/pic/banner.jpg
ADDED
|
Binary file
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Provision a new Velaclaw member runtime
|
|
2
|
+
|
|
3
|
+
This guide provisions a new member runtime from `members/member-template/`.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Each member gets:
|
|
8
|
+
- private memory
|
|
9
|
+
- private skills
|
|
10
|
+
- private tool bindings
|
|
11
|
+
- private docs
|
|
12
|
+
- isolated OpenClaw runtime config/workspace/secrets/logs
|
|
13
|
+
|
|
14
|
+
## Steps
|
|
15
|
+
|
|
16
|
+
### 1. Copy the template
|
|
17
|
+
|
|
18
|
+
From `velaclaw/members/`:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cp -a member-template <member-id>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Edit runtime config
|
|
25
|
+
|
|
26
|
+
File:
|
|
27
|
+
- `members/<member-id>/runtime/config/openclaw.json`
|
|
28
|
+
|
|
29
|
+
Replace:
|
|
30
|
+
- `REPLACE_WITH_RANDOM_GATEWAY_TOKEN`
|
|
31
|
+
- `REPLACE_WITH_USER_ID`
|
|
32
|
+
|
|
33
|
+
Optional:
|
|
34
|
+
- assistant identity name
|
|
35
|
+
- model defaults
|
|
36
|
+
- disabled tools
|
|
37
|
+
|
|
38
|
+
### 3. Add member channel secrets
|
|
39
|
+
|
|
40
|
+
Place secrets in:
|
|
41
|
+
- `members/<member-id>/runtime/secrets/`
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
- `telegram-bot-token`
|
|
45
|
+
|
|
46
|
+
### 4. Adjust compose settings
|
|
47
|
+
|
|
48
|
+
File:
|
|
49
|
+
- `members/<member-id>/runtime/docker-compose.yml`
|
|
50
|
+
|
|
51
|
+
Replace:
|
|
52
|
+
- `REPLACE_MEMBER_ID`
|
|
53
|
+
- published port `18800` if another member already uses it
|
|
54
|
+
|
|
55
|
+
### 5. Start runtime
|
|
56
|
+
|
|
57
|
+
From the member runtime directory:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
docker compose up -d
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 6. Verify isolation
|
|
64
|
+
|
|
65
|
+
Check:
|
|
66
|
+
- container is healthy
|
|
67
|
+
- only the member's own workspace is mounted
|
|
68
|
+
- no docker socket is mounted
|
|
69
|
+
- member secrets stay local to that runtime
|
|
70
|
+
- high-risk tools remain disabled or approval-gated
|
|
71
|
+
|
|
72
|
+
## Naming recommendations
|
|
73
|
+
|
|
74
|
+
Use stable member IDs such as:
|
|
75
|
+
- `zane`
|
|
76
|
+
- `alice`
|
|
77
|
+
- `research`
|
|
78
|
+
- `ops`
|
|
79
|
+
|
|
80
|
+
## Safety checklist
|
|
81
|
+
|
|
82
|
+
- unique bot token per member runtime
|
|
83
|
+
- unique allowlist user ID(s)
|
|
84
|
+
- unique published port if exposing locally
|
|
85
|
+
- no host primary workspace mounts
|
|
86
|
+
- no host docker socket mounts
|
|
87
|
+
- no shared writable team asset mounts
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
acceptInvitationByCode,
|
|
8
|
+
createInvitationForTeam,
|
|
9
|
+
createTeam,
|
|
10
|
+
createTeamAssetProposal,
|
|
11
|
+
getTeamAssetCapabilityRegistryBySlug,
|
|
12
|
+
getTeamAssetServerManifestBySlug,
|
|
13
|
+
getTeamMemberWorkspaceById,
|
|
14
|
+
runMemberRuntimeActionForTeam
|
|
15
|
+
} from "../dist/data.js";
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const ROOT = process.env.VELACLAW_ROOT
|
|
19
|
+
? path.resolve(process.env.VELACLAW_ROOT)
|
|
20
|
+
: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
+
|
|
22
|
+
async function run(command, args, options = {}) {
|
|
23
|
+
const result = await execFileAsync(command, args, {
|
|
24
|
+
cwd: options.cwd || ROOT,
|
|
25
|
+
maxBuffer: 1024 * 1024 * 24
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
stdout: String(result.stdout || ""),
|
|
29
|
+
stderr: String(result.stderr || "")
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensureReadTool(configPath) {
|
|
34
|
+
const raw = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
35
|
+
raw.tools ??= {};
|
|
36
|
+
raw.tools.alsoAllow = Array.from(new Set([...(Array.isArray(raw.tools.alsoAllow) ? raw.tools.alsoAllow : []), "read"]));
|
|
37
|
+
raw.tools.fs = {
|
|
38
|
+
...(typeof raw.tools.fs === "object" && raw.tools.fs ? raw.tools.fs : {}),
|
|
39
|
+
workspaceOnly: true
|
|
40
|
+
};
|
|
41
|
+
await fs.writeFile(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function waitForRuntimeHealthy(teamSlug, memberId, timeoutMs = 120000) {
|
|
45
|
+
const started = Date.now();
|
|
46
|
+
while (Date.now() - started < timeoutMs) {
|
|
47
|
+
const { stdout } = await run("curl", ["-sS", `http://127.0.0.1:4318/api/teams/${teamSlug}/members/${memberId}/runtime`]);
|
|
48
|
+
const payload = JSON.parse(stdout);
|
|
49
|
+
if (payload?.runtime?.container?.health === "healthy") {
|
|
50
|
+
return payload.runtime;
|
|
51
|
+
}
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`runtime did not become healthy for ${memberId}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function findSessionEntry(sessionsPath, suffix, timeoutMs = 30000) {
|
|
58
|
+
const started = Date.now();
|
|
59
|
+
while (Date.now() - started < timeoutMs) {
|
|
60
|
+
const raw = JSON.parse(await fs.readFile(sessionsPath, "utf8"));
|
|
61
|
+
const key = Object.keys(raw).find((entry) => entry.includes(suffix));
|
|
62
|
+
if (key) {
|
|
63
|
+
return {
|
|
64
|
+
sessionKey: key,
|
|
65
|
+
sessionId: raw[key].sessionId,
|
|
66
|
+
entry: raw[key]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`session not found for ${suffix}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildSyntheticAssets() {
|
|
75
|
+
return {
|
|
76
|
+
skills: [
|
|
77
|
+
{
|
|
78
|
+
title: "Thesis Merge Composer",
|
|
79
|
+
content: `---
|
|
80
|
+
name: thesis-merge-composer
|
|
81
|
+
description: Merge multiple short drafts into one coherent final memo. Use when the user provides two or more fragments and wants one integrated output.
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
# Thesis Merge Composer
|
|
85
|
+
|
|
86
|
+
Use this skill when multiple short drafts need to become one coherent memo.
|
|
87
|
+
|
|
88
|
+
Rules:
|
|
89
|
+
- Preserve all meaningful arguments from each draft.
|
|
90
|
+
- Remove repetition.
|
|
91
|
+
- Produce one integrated final memo, not a bullet outline.
|
|
92
|
+
- Prefer natural Chinese prose over formal report boilerplate.
|
|
93
|
+
`
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
title: "Consensus Stress Tester",
|
|
97
|
+
content: `---
|
|
98
|
+
name: consensus-stress-tester
|
|
99
|
+
description: Challenge consensus views and identify what the market may already be pricing in. Use for investment or strategic analysis.
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
# Consensus Stress Tester
|
|
103
|
+
|
|
104
|
+
When a thesis sounds obvious, ask:
|
|
105
|
+
- what is already consensus?
|
|
106
|
+
- what assumptions are embedded in the current narrative?
|
|
107
|
+
- what operational bottlenecks could delay or weaken the expected outcome?
|
|
108
|
+
|
|
109
|
+
Always add a section that stresses the consensus view before giving a final conclusion.
|
|
110
|
+
`
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: "CN Final Polish",
|
|
114
|
+
content: `---
|
|
115
|
+
name: cn-final-polish
|
|
116
|
+
description: Turn a stiff or AI-sounding Chinese draft into more natural, direct prose.
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
# CN Final Polish
|
|
120
|
+
|
|
121
|
+
Use this skill for the last pass of Chinese writing.
|
|
122
|
+
|
|
123
|
+
Rules:
|
|
124
|
+
- Remove empty transition phrases.
|
|
125
|
+
- Prefer direct sentences.
|
|
126
|
+
- Avoid exaggerated slogan-like endings.
|
|
127
|
+
- Keep the tone calm, concrete, and readable.
|
|
128
|
+
`
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
memory: {
|
|
132
|
+
title: "Team Preference Ledger",
|
|
133
|
+
content: `# Team Preference Ledger
|
|
134
|
+
|
|
135
|
+
When this workspace writes a final investment memo, include the exact phrase "风险是按时间定价的" once in the final answer.
|
|
136
|
+
`
|
|
137
|
+
},
|
|
138
|
+
workflow: {
|
|
139
|
+
title: "Investment Memo Flow",
|
|
140
|
+
content: `# Investment Memo Flow
|
|
141
|
+
|
|
142
|
+
Follow these three sections in order:
|
|
143
|
+
|
|
144
|
+
1. Merged View
|
|
145
|
+
2. Second-Layer Risks
|
|
146
|
+
3. Final Memo
|
|
147
|
+
|
|
148
|
+
Use these section headings exactly in the final answer.
|
|
149
|
+
`
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function createPublishedAssets(teamSlug) {
|
|
155
|
+
const assets = buildSyntheticAssets();
|
|
156
|
+
const created = [];
|
|
157
|
+
|
|
158
|
+
for (const skill of assets.skills) {
|
|
159
|
+
const result = await createTeamAssetProposal({
|
|
160
|
+
teamSlug,
|
|
161
|
+
category: "shared-skills",
|
|
162
|
+
title: skill.title,
|
|
163
|
+
content: skill.content,
|
|
164
|
+
submittedByLabel: "manager",
|
|
165
|
+
sourceZone: "collab"
|
|
166
|
+
});
|
|
167
|
+
created.push({ type: "skill", title: skill.title, assetId: result.asset.id, status: result.asset.status });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const [type, category, payload] of [
|
|
171
|
+
["memory", "shared-memory", assets.memory],
|
|
172
|
+
["workflow", "shared-workflows", assets.workflow]
|
|
173
|
+
]) {
|
|
174
|
+
const result = await createTeamAssetProposal({
|
|
175
|
+
teamSlug,
|
|
176
|
+
category,
|
|
177
|
+
title: payload.title,
|
|
178
|
+
content: payload.content,
|
|
179
|
+
submittedByLabel: "manager",
|
|
180
|
+
sourceZone: "collab"
|
|
181
|
+
});
|
|
182
|
+
created.push({ type, title: payload.title, assetId: result.asset.id, status: result.asset.status });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return created;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function runMemberScenario(containerName, sessionId, prompt) {
|
|
189
|
+
const { stdout } = await run("sudo", [
|
|
190
|
+
"-n",
|
|
191
|
+
"docker",
|
|
192
|
+
"exec",
|
|
193
|
+
containerName,
|
|
194
|
+
"sh",
|
|
195
|
+
"-lc",
|
|
196
|
+
`cd /app && openclaw agent --session-id ${sessionId} --message ${JSON.stringify(prompt)} --json`
|
|
197
|
+
]);
|
|
198
|
+
return JSON.parse(stdout);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function readSessionArtifacts(memberRoot, suffix) {
|
|
202
|
+
const sessionsPath = path.join(memberRoot, "config", "agents", "main", "sessions", "sessions.json");
|
|
203
|
+
const { sessionId, sessionKey, entry } = await findSessionEntry(sessionsPath, suffix);
|
|
204
|
+
const jsonlPath = path.join(memberRoot, "config", "agents", "main", "sessions", `${sessionId}.jsonl`);
|
|
205
|
+
const jsonlLines = (await fs.readFile(jsonlPath, "utf8"))
|
|
206
|
+
.trim()
|
|
207
|
+
.split("\n")
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.map((line) => JSON.parse(line));
|
|
210
|
+
|
|
211
|
+
const assistantToolCalls = jsonlLines
|
|
212
|
+
.filter((item) => item.type === "message" && item.message?.role === "assistant")
|
|
213
|
+
.flatMap((item) => item.message.content || [])
|
|
214
|
+
.filter((item) => item.type === "toolCall")
|
|
215
|
+
.map((item) => ({
|
|
216
|
+
name: item.name,
|
|
217
|
+
arguments: item.arguments
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
const finalAssistantText =
|
|
221
|
+
jsonlLines
|
|
222
|
+
.filter((item) => item.type === "message" && item.message?.role === "assistant")
|
|
223
|
+
.flatMap((item) => item.message.content || [])
|
|
224
|
+
.filter((item) => item.type === "text")
|
|
225
|
+
.map((item) => item.text)
|
|
226
|
+
.join("\n\n") || "";
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
sessionId,
|
|
230
|
+
sessionKey,
|
|
231
|
+
entry,
|
|
232
|
+
assistantToolCalls,
|
|
233
|
+
finalAssistantText,
|
|
234
|
+
jsonlPath
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function main() {
|
|
239
|
+
await fs.mkdir(path.join(ROOT, "artifacts"), { recursive: true });
|
|
240
|
+
const teamSuffix = Date.now();
|
|
241
|
+
const team = await createTeam({
|
|
242
|
+
name: `Asset Stack Test ${teamSuffix}`,
|
|
243
|
+
slug: `asset-stack-test-${teamSuffix}`,
|
|
244
|
+
description: "Synthetic skills/memory/workflow combination regression test",
|
|
245
|
+
managerLabel: "Test Manager"
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const createdAssets = await createPublishedAssets(team.slug);
|
|
249
|
+
const invitation = await createInvitationForTeam(team.slug, {
|
|
250
|
+
inviteeLabel: "Asset Stack Probe",
|
|
251
|
+
memberId: "asset-stack-probe@team.local",
|
|
252
|
+
memberEmail: "asset-stack-probe@team.local",
|
|
253
|
+
role: "member",
|
|
254
|
+
createdBy: "shared-asset-stack-test"
|
|
255
|
+
});
|
|
256
|
+
const accepted = await acceptInvitationByCode(invitation.code, {
|
|
257
|
+
identityName: "Asset Stack Probe"
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const memberId = accepted.policy.memberId;
|
|
261
|
+
const memberRoot = path.join(ROOT, "members", team.slug, memberId, "runtime");
|
|
262
|
+
const configPath = path.join(memberRoot, "config", "openclaw.json");
|
|
263
|
+
await ensureReadTool(configPath);
|
|
264
|
+
await runMemberRuntimeActionForTeam(team.slug, memberId, "start");
|
|
265
|
+
const runtime = await waitForRuntimeHealthy(team.slug, memberId);
|
|
266
|
+
|
|
267
|
+
const probeWorkspace = await getTeamMemberWorkspaceById(team.slug, memberId);
|
|
268
|
+
const registry = await getTeamAssetCapabilityRegistryBySlug(team.slug);
|
|
269
|
+
const workspaceSkillFiles = (await fs.readdir(path.join(memberRoot, "workspace", "skills"))).sort();
|
|
270
|
+
|
|
271
|
+
const skillMemoryPrompt = [
|
|
272
|
+
"请整合下面两段投资草稿,先挑战市场共识,再给我一版自然的中文最终稿。",
|
|
273
|
+
"",
|
|
274
|
+
"草稿A:市场普遍认为该公司会因为 AI 数据中心扩张而快速增长。",
|
|
275
|
+
"草稿B:审批、资本开支和供应链可能让业绩兑现更慢。",
|
|
276
|
+
"",
|
|
277
|
+
"如果团队有固定写作偏好,请遵守。"
|
|
278
|
+
].join("\n");
|
|
279
|
+
|
|
280
|
+
const workflowPrompt = [
|
|
281
|
+
"请按团队共享流程输出一版投资分析。",
|
|
282
|
+
"",
|
|
283
|
+
"输入观点:公司受益于 AI 数据中心电力需求,但兑现节奏可能慢于市场预期。",
|
|
284
|
+
"",
|
|
285
|
+
"请严格按共享流程输出最终结果。"
|
|
286
|
+
].join("\n");
|
|
287
|
+
|
|
288
|
+
const skillMemoryResult = await runMemberScenario(runtime.containerName, "asset-stack-skill-memory", skillMemoryPrompt);
|
|
289
|
+
const workflowResult = await runMemberScenario(runtime.containerName, "asset-stack-workflow", workflowPrompt);
|
|
290
|
+
|
|
291
|
+
const skillMemorySession = await readSessionArtifacts(memberRoot, "asset-stack-skill-memory");
|
|
292
|
+
const workflowSession = await readSessionArtifacts(memberRoot, "asset-stack-workflow");
|
|
293
|
+
|
|
294
|
+
const report = {
|
|
295
|
+
createdAt: new Date().toISOString(),
|
|
296
|
+
team,
|
|
297
|
+
member: {
|
|
298
|
+
memberId,
|
|
299
|
+
runtime,
|
|
300
|
+
workspaceSkillFiles
|
|
301
|
+
},
|
|
302
|
+
createdAssets,
|
|
303
|
+
registryCounts: registry.counts,
|
|
304
|
+
registryTitles: Object.fromEntries(
|
|
305
|
+
Object.entries(registry.byKind).map(([kind, items]) => [kind, items.map((item) => item.title)])
|
|
306
|
+
),
|
|
307
|
+
scenarios: {
|
|
308
|
+
skillMemory: {
|
|
309
|
+
sessionId: skillMemorySession.sessionId,
|
|
310
|
+
loadedSkills: (skillMemorySession.entry.skillsSnapshot?.skills || []).map((entry) => entry.name),
|
|
311
|
+
toolCalls: skillMemorySession.assistantToolCalls,
|
|
312
|
+
finalAssistantText:
|
|
313
|
+
skillMemoryResult?.result?.payloads?.map((entry) => entry.text).filter(Boolean).join("\n\n") ||
|
|
314
|
+
skillMemorySession.finalAssistantText
|
|
315
|
+
},
|
|
316
|
+
workflow: {
|
|
317
|
+
sessionId: workflowSession.sessionId,
|
|
318
|
+
loadedSkills: (workflowSession.entry.skillsSnapshot?.skills || []).map((entry) => entry.name),
|
|
319
|
+
toolCalls: workflowSession.assistantToolCalls,
|
|
320
|
+
finalAssistantText:
|
|
321
|
+
workflowResult?.result?.payloads?.map((entry) => entry.text).filter(Boolean).join("\n\n") ||
|
|
322
|
+
workflowSession.finalAssistantText
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
checks: {
|
|
326
|
+
sharedSkillsDiscovered:
|
|
327
|
+
workspaceSkillFiles.filter((name) => name.startsWith("team-shared-")).length >= 3,
|
|
328
|
+
gatewayUsedForRuns:
|
|
329
|
+
skillMemorySession.entry.modelProvider === "team-gateway" &&
|
|
330
|
+
workflowSession.entry.modelProvider === "team-gateway",
|
|
331
|
+
skillMemoryReadsSharedSkills:
|
|
332
|
+
skillMemorySession.assistantToolCalls.some((call) => call.name === "read" && String(call.arguments?.path || "").includes("team-shared-")),
|
|
333
|
+
skillMemoryReadsSharedMemory:
|
|
334
|
+
skillMemorySession.assistantToolCalls.some((call) => {
|
|
335
|
+
const targetPath = String(call.arguments?.path || "");
|
|
336
|
+
return call.name === "read" && (
|
|
337
|
+
targetPath.includes("/MEMORY.md") ||
|
|
338
|
+
targetPath.includes("/docs/team-shared/active/memory/") ||
|
|
339
|
+
targetPath.includes("team-shared-memory-")
|
|
340
|
+
);
|
|
341
|
+
}) ||
|
|
342
|
+
Array.isArray(skillMemorySession.entry.systemPromptReport?.injectedWorkspaceFiles) &&
|
|
343
|
+
skillMemorySession.entry.systemPromptReport.injectedWorkspaceFiles.some((file) => file?.name === "MEMORY.md" && file?.missing === false),
|
|
344
|
+
skillMemoryOutputReflectsMemory:
|
|
345
|
+
(skillMemoryResult?.result?.payloads?.map((entry) => entry.text).join("\n") || "").includes("风险是按时间定价的"),
|
|
346
|
+
workflowReadsSharedWorkflow:
|
|
347
|
+
workflowSession.assistantToolCalls.some((call) => {
|
|
348
|
+
const targetPath = String(call.arguments?.path || "");
|
|
349
|
+
return call.name === "read" && (
|
|
350
|
+
targetPath.includes("/docs/team-shared/active/workflows/") ||
|
|
351
|
+
targetPath.includes("team-shared-workflow-")
|
|
352
|
+
);
|
|
353
|
+
}),
|
|
354
|
+
workflowOutputUsesExpectedSections:
|
|
355
|
+
["Merged View", "Second-Layer Risks", "Final Memo"].every((heading) =>
|
|
356
|
+
(workflowResult?.result?.payloads?.map((entry) => entry.text).join("\n") || "").includes(heading)
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const artifactPath = path.join(ROOT, "artifacts", `shared-asset-stack-test-${teamSuffix}.json`);
|
|
362
|
+
await fs.writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
363
|
+
console.log(JSON.stringify({ artifactPath, report }, null, 2));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
main().catch((error) => {
|
|
367
|
+
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
368
|
+
process.exitCode = 1;
|
|
369
|
+
});
|