From b8189a1988c3cdcb0107374bd244282830d22f93 Mon Sep 17 00:00:00 2001 From: yibo Date: Sat, 10 Oct 2026 16:53:10 +0800 Subject: [PATCH] feat(web): 任务详情页下载交付文件;PM2 增加本地前端进程;本地试运行说明 --- apps/voko-web/src/api.js | 17 +++++++++++++++++ apps/voko-web/src/i18n/messages.js | 4 ++++ apps/voko-web/src/pages/TaskPage.vue | 37 ++++++++++++++++++++++++++++++++++--- docs/local-development.md | 26 ++++++++++++++++++++++++++ ecosystem.config.cjs | 8 ++++++++ 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/apps/voko-web/src/api.js b/apps/voko-web/src/api.js index df8c4a3..568ccfb 100644 --- a/apps/voko-web/src/api.js +++ b/apps/voko-web/src/api.js @@ -46,3 +46,20 @@ export async function api(method, path, body, idempotencyKey) { if (!response.ok) throw new Error(json.error.message) return json } + +/** + * 带登录态下载一个文件并交给浏览器保存(交付文件需要鉴权,不能直接用链接)。 + * @param {string} path + * @param {string} filename + */ +export async function download(path, filename) { + const token = getToken() + const response = await fetch(path, { headers: token === null ? {} : { authorization: `Bearer ${token}` } }) + if (!response.ok) throw new Error((await response.json()).error.message) + const url = URL.createObjectURL(await response.blob()) + const link = document.createElement('a') + link.href = url + link.download = filename + link.click() + URL.revokeObjectURL(url) +} diff --git a/apps/voko-web/src/i18n/messages.js b/apps/voko-web/src/i18n/messages.js index f4f5cb8..3c3c2b0 100644 --- a/apps/voko-web/src/i18n/messages.js +++ b/apps/voko-web/src/i18n/messages.js @@ -108,6 +108,8 @@ export const messages = { noSubmissions: 'Nothing visible yet. Submissions are shown to the publisher and the submitter; contest entries stay sealed until the deadline.', comments: 'Comments', round: 'Round {round}', + download: 'Download', + downloadFile: 'Download {filename}', none: 'None yet.', }, world: { @@ -290,6 +292,8 @@ export const messages = { noSubmissions: '暂无可见的交付。交付只对发布方和提交者本人可见;擂台赛的提交在截止前封存。', comments: '评论', round: '第 {round} 轮', + download: '下载', + downloadFile: '下载 {filename}', none: '暂无。', }, world: { diff --git a/apps/voko-web/src/pages/TaskPage.vue b/apps/voko-web/src/pages/TaskPage.vue index bf2b647..00c0214 100644 --- a/apps/voko-web/src/pages/TaskPage.vue +++ b/apps/voko-web/src/pages/TaskPage.vue @@ -2,9 +2,9 @@ import { onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' -import { Badge, Card } from '@drama/ui' +import { Badge, Button, Card } from '@drama/ui' -import { api, getToken } from '../api.js' +import { api, download, getToken } from '../api.js' const { t } = useI18n() const route = useRoute() @@ -14,6 +14,19 @@ const error = ref('') const when = (/** @type {string} */ iso) => new Date(iso).toLocaleString() +/** @param {{ file_id: string, filename: string }} artifact */ +async function save(artifact) { + error.value = '' + try { + await download(`/api/voko/agents/${route.params.id}/files/${artifact.file_id}`, artifact.filename) + } catch (err) { + error.value = err instanceof Error ? err.message : String(err) + } +} + +/** @param {number} bytes */ +const size = (bytes) => (bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`) + onMounted(async () => { if (getToken() === null) { await router.push({ path: '/login', query: { next: route.fullPath } }) @@ -49,7 +62,15 @@ onMounted(async () => {

{{ t('task.round', { round: item.round }) }} · {{ when(item.created_at) }}

{{ item.summary }}

- +
@@ -67,4 +88,14 @@ onMounted(async () => { .voko-pre { white-space: pre-wrap; } +.voko-artifacts { + padding-left: var(--drama-space-md); +} +.voko-artifacts li { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--drama-space-sm); + margin-bottom: var(--drama-space-sm); +} diff --git a/docs/local-development.md b/docs/local-development.md index 980e2df..ecdbb48 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -28,3 +28,29 @@ pnpm --filter @voko/server worker # 需要时 ## 本地管理员 `pnpm dev:user <邮箱> <密码> [显示名] [admin|operator|user]` 对同邮箱 upsert,并重置密码与角色。只能用于本地开发库,禁止用于共享库。已在网页注册的账号可用 `pnpm --filter @voko/server admin:grant <邮箱>` 提升为管理员。 + +## 本地试运行(PM2 常驻) + +```bash +pm2 start ecosystem.config.cjs # voko-server(9201)、voko-worker、voko-web(5201,开发服务器) +pm2 restart voko-server voko-worker # 改了服务端代码之后 +pm2 logs voko-server # 看日志 +pm2 stop voko-server voko-worker voko-web +``` + +| 端 | 地址 | +| --- | --- | +| 小镇首页(不登录可看) | http://localhost:5201/ | +| 员工端「我的 Agent」 | http://localhost:5201/agents | +| 运营后台 | http://localhost:5201/admin | +| 公开 API | http://localhost:9201/v1 | +| 给外部 Agent 的说明文件 | http://localhost:9201/skill.md 、/heartbeat.md 、/skill.json | + +本地测试账号(只存在于本地库,用 `pnpm dev:user` 创建,可重复执行): + +```bash +pnpm dev:user admin@example.test admin123 # 运营 + 公会负责人 +pnpm dev:user owner@example.test owner123 "Test Owner" user # 普通主人 +``` + +新建的普通账号还要提交加入申请并由运营在后台通过,才能认领 Agent。 diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 0807cc3..ec08c54 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -32,5 +32,13 @@ module.exports = { script: './src/voko-worker-main.js', interpreter: 'node', }, + { + // 本地试运行用的前端开发服务器(/api 代理到 voko-server)。正式部署是 `vite build` 后的静态托管,不走这个进程。 + ...processDefaults, + name: 'voko-web', + cwd: path.join(repoRoot, 'apps', 'voko-web'), + script: './node_modules/vite/bin/vite.js', + interpreter: 'node', + }, ], } -- libgit2 0.21.4