Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions demohouse/personal-investment-assistant/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

jobs:
verify:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.13'
cache: npm
cache-dependency-path: app/package-lock.json
- run: npm ci
working-directory: app
- run: npm run verify
32 changes: 29 additions & 3 deletions demohouse/personal-investment-assistant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,40 @@
帮我初始化个人投资助手:https://github.com/volcengine/ai-app-lab/blob/main/demohouse/personal-investment-assistant/skills/investment-assistant/SKILL.md
```

该地址直接指向中文 Skill 入口。Codex 或 Claude Code 会获取完整仓库,安装与当前客户端匹配的 Skill,并进入同一套初始化流程。
该地址是 AI App Lab 的官方入口。日常可以使用 `main` 获取官方最新版本;重大宣传应在
AI App Lab 更新 PR 合并后,把 `main` 替换为该次合并的完整 40 位 commit SHA,避免入口漂移。

需要固定版本时,也可以使用独立仓库中的 `v0.3.0` 入口:
官方 URL 只负责让 Agent 找到 Skill。Skill 会固定下载并验证以下独立发行版本,不会下载
整个 AI App Lab,也不会使用独立仓库的 `main`:

```text
帮我初始化个人投资助手:https://github.com/3494036618-eng/personal-investment-assistant/blob/v0.3.0/skills/investment-assistant/SKILL.md
固定发行仓库:https://github.com/3494036618-eng/personal-investment-assistant/tree/v0.3.1
固定发行 Skill:https://github.com/3494036618-eng/personal-investment-assistant/blob/v0.3.1/skills/investment-assistant/SKILL.md
```

维护者可以生成日常官方口令或宣传固定口令:

```bash
npm run skill:command -- --official
npm run skill:command -- --official-ref <AI App Lab完整40位commit SHA>
```

发布 `v0.3.1` 前必须完成 Ubuntu、macOS、Windows CI;正式 GitHub Release 必须启用
immutable。发布后先从公开 GitHub 全新下载并验收:

```bash
npm run release:verify:public
```

AI App Lab 更新合并后,再校验固定 commit、完整项目镜像和 `UPSTREAM.json`:

```bash
npm run release:verify:public -- --official-ref <AI App Lab完整40位commit SHA>
```

这两次验证不会调用 Agent Plan、DataPro 或豆包搜索,也不会产生 AFP。只有在用户确认投资
配置和真实调用后,初始化流程才会探测 Provider 并生成报告。

已经安装中文 Skill 后:

```text
Expand Down
6 changes: 6 additions & 0 deletions demohouse/personal-investment-assistant/UPSTREAM.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"repository": "https://github.com/3494036618-eng/personal-investment-assistant",
"commit": "ddb626703187baeae067d61f781f46e16e1503c3",
"version": "0.3.1",
"synced_at": "2026-09-03"
}
64 changes: 32 additions & 32 deletions demohouse/personal-investment-assistant/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demohouse/personal-investment-assistant/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@investment-assistant/app",
"version": "0.3.0",
"version": "0.3.1",
"private": true,
"type": "module",
"engines": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ function fixture() {

const randomId = '00000000-0000-4000-8000-000000000001';

function assertPrivateFile(filePath) {
assert.equal(fs.statSync(filePath).isFile(), true);
if (process.platform !== 'win32') {
assert.equal(fs.statSync(filePath).mode & 0o777, 0o600);
}
}

test('serves the built frontend and requires a fresh live provider check for readiness', async (t) => {
const context = fixture();
t.after(() => context.close());
Expand All @@ -77,9 +84,9 @@ test('serves the built frontend and requires a fresh live provider check for rea
assert.equal(ready.body.live_check.fresh, true);
assert.equal(ready.body.live_check.providers.web_search.ok, true);
assert.equal(ready.body.live_check.providers.agent_plan_model.ok, true);
assert.equal(fs.statSync(context.config.providerHealthPath).mode & 0o777, 0o600);
assertPrivateFile(context.config.providerHealthPath);
for (const suffix of ['', '-wal', '-shm']) {
assert.equal(fs.statSync(`${context.config.databasePath}${suffix}`).mode & 0o777, 0o600);
assertPrivateFile(`${context.config.databasePath}${suffix}`);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import { createDatabase } from '../../src/server/db/database.js';
test('migrates legacy monitor tables without losing settings or run history', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'investment-assistant-db-'));
const databasePath = path.join(directory, 'legacy.sqlite');
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
let repository;
t.after(() => {
try {
repository?.close();
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});

const legacy = new DatabaseSync(databasePath);
legacy.exec(`
Expand Down Expand Up @@ -63,8 +70,7 @@ test('migrates legacy monitor tables without losing settings or run history', (t
`).run();
legacy.close();

const repository = createDatabase(databasePath);
t.after(() => repository.close());
repository = createDatabase(databasePath);

const settings = repository.getMonitorSettings('stock-1');
assert.equal(settings.enabled, true);
Expand Down Expand Up @@ -108,10 +114,16 @@ test('migrates legacy monitor tables without losing settings or run history', (t
test('lists every saved report when no explicit history limit is requested', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'investment-assistant-history-'));
const databasePath = path.join(directory, 'history.sqlite');
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
let repository;
t.after(() => {
try {
repository?.close();
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});

const repository = createDatabase(databasePath);
t.after(() => repository.close());
repository = createDatabase(databasePath);
const stock = repository.createStock({
name: '示例科技',
code: '000001.SZ',
Expand Down Expand Up @@ -150,7 +162,14 @@ test('lists every saved report when no explicit history limit is requested', (t)
test('never persists raw web-search content in report_json', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'investment-assistant-storage-'));
const databasePath = path.join(directory, 'storage.sqlite');
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
let reopened;
t.after(() => {
try {
reopened?.close();
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});

const repository = createDatabase(databasePath);
const stock = repository.createStock({
Expand Down Expand Up @@ -217,8 +236,7 @@ test('never persists raw web-search content in report_json', (t) => {
raw.close();
assert.doesNotMatch(storedJson, /RAW_DATABASE_PROVIDER_/);

const reopened = createDatabase(databasePath);
t.after(() => reopened.close());
reopened = createDatabase(databasePath);
const source = reopened.getLatestReport(stock.id, 'brief').report.evidence[0];
assert.equal(source.title, '示例科技发布最新产品');
assert.equal(source.url, 'https://example.com/product');
Expand All @@ -228,7 +246,14 @@ test('never persists raw web-search content in report_json', (t) => {
test('migrates raw web-search content already stored by an earlier version', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'investment-assistant-report-migration-'));
const databasePath = path.join(directory, 'migration.sqlite');
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
let migrated;
t.after(() => {
try {
migrated?.close();
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
});

const repository = createDatabase(databasePath);
const stock = repository.createStock({
Expand Down Expand Up @@ -289,8 +314,7 @@ test('migrates raw web-search content already stored by an earlier version', (t)
);
legacy.close();

const migrated = createDatabase(databasePath);
t.after(() => migrated.close());
migrated = createDatabase(databasePath);
const report = migrated.getLatestReport(stock.id, 'brief').report;
const serialized = JSON.stringify(report);

Expand Down
Loading
Loading