chore: consolidate admin cleanup and colima config
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
TKMIND_API_TARGET=https://127.0.0.1:18006
|
||||
MEMIND_LIB_ROOT=/Users/john/Project/Memind
|
||||
VITE_MAIN_APP_URL=http://127.0.0.1:5173
|
||||
+12
-2
@@ -16,7 +16,7 @@ DATABASE_URL=mysql://boot:password@host:3306/goose
|
||||
# MYSQL_DATABASE=goose
|
||||
|
||||
# 用户工作区根目录(创建用户时使用)
|
||||
H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users
|
||||
H5_USERS_ROOT=/Users/john/Project/Memind/users
|
||||
|
||||
# 管理员(可选;设置后启动时同步到数据库)
|
||||
# H5_ADMIN_USERNAME=admin
|
||||
@@ -25,6 +25,11 @@ H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users
|
||||
# LLM 配置同步(可选)
|
||||
# TKMIND_API_TARGET=https://127.0.0.1:18006
|
||||
# TKMIND_SERVER__SECRET_KEY=local-dev-secret
|
||||
NODE_BIN=/opt/homebrew/opt/node@24/bin/node
|
||||
AIDER_BIN=/Users/john/Project/aider/aider.sh
|
||||
GOOSE_AIDER_BIN=/Users/john/Project/aider/aider.sh
|
||||
OPENHANDS_BIN=/Users/john/.openhands/bin/studio-openhands
|
||||
GOOSE_OPENHANDS_BIN=/Users/john/.openhands/bin/studio-openhands
|
||||
|
||||
# ── Skill 系统 ──
|
||||
# goosed 节点地址(逗号分隔支持多节点;本地单节点只填一个)
|
||||
@@ -36,6 +41,11 @@ H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users
|
||||
# GOOSE_SERVER__SECRET_KEY=local-dev-secret
|
||||
# skill 执行超时(毫秒,默认 120000)
|
||||
# SKILL_GOOSED_TIMEOUT_MS=120000
|
||||
# goosed skill 会话工作目录(默认 /tmp)
|
||||
# GOOSE_SKILL_WORKING_DIR=/tmp
|
||||
# goosed LLM provider(与 goosed 本地 config.yaml 中的 active_provider 对应)
|
||||
# GOOSE_PROVIDER=custom_deepseek
|
||||
# GOOSE_MODEL=deepseek-reasoner
|
||||
|
||||
# 套餐同步到生产后台(可选;配置后套餐增删改会自动镜像到目标后台)
|
||||
# PLAN_SYNC_TARGET_BASE_URL=https://md.tkmind.cn
|
||||
@@ -44,7 +54,7 @@ H5_USERS_ROOT=/Users/john/Project/memind_adm/data/users
|
||||
# PLAN_SYNC_TIMEOUT_MS=10000
|
||||
|
||||
# Memind 业务模块路径(user-auth / llm-providers 等,默认 ../Memind)
|
||||
# MEMIND_LIB_ROOT=/Users/john/Project/Memind
|
||||
MEMIND_LIB_ROOT=/Users/john/Project/Memind
|
||||
|
||||
# 超管页「返回对话」跳转主 H5(可选)
|
||||
VITE_MAIN_APP_URL=https://h5.tkmind.cn
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
*.local
|
||||
node_modules/
|
||||
dist/
|
||||
@@ -9,3 +10,5 @@ memind-lib/
|
||||
.vite/
|
||||
*.log
|
||||
logs/
|
||||
.mindops/
|
||||
.adm-*.pid
|
||||
|
||||
+5
-5
@@ -59,13 +59,13 @@
|
||||
|
||||
## gadm 独立登录
|
||||
|
||||
`https://gadm.tkmind.cn` 应部署 **本仓库 memind_adm**,勿再反代到 `Memind/ops` Vite dev(旧版未登录会跳 `localhost:5173`)。
|
||||
`https://md.tkmind.cn` 应部署 **本仓库 memind_adm**,勿再反代到 `Memind/ops` Vite dev(旧版未登录会跳 `localhost:5173`)。
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 登录 | 本页 `/auth/login`,账号来自共用 MySQL |
|
||||
| 会话 | 设置 `H5_PUBLIC_BASE_URL=https://gadm.tkmind.cn`,**不要**设 `H5_COOKIE_DOMAIN=.tkmind.cn` |
|
||||
| 路径 | `https://gadm.tkmind.cn/` 直达超管后台;Plaza 运营在 `/ops`;`/ops/admin` 已废弃 |
|
||||
| 会话 | 设置 `H5_PUBLIC_BASE_URL=https://md.tkmind.cn`,**不要**设 `H5_COOKIE_DOMAIN=.tkmind.cn` |
|
||||
| 路径 | `https://md.tkmind.cn/` 直达超管后台;Plaza 运营在 `/ops`;`/ops/admin` 已废弃 |
|
||||
| API | `ADM_DEV_BACKEND=http://127.0.0.1:8085`(preview 反代目标) |
|
||||
| 初始化 admin | `npm run admin:init` 只更新 `Memind` 主用户表中的 admin 密码,不创建独立后台账号 |
|
||||
|
||||
@@ -187,7 +187,7 @@ ssh john@10.10.0.2 'tail -f /Users/john/Project/memind_adm/adm-preview.log'
|
||||
# preview 反代目标(/auth、/admin-api、/api)— 指向本仓库 Admin API
|
||||
ADM_API_PORT=8085
|
||||
ADM_DEV_BACKEND=http://127.0.0.1:8085
|
||||
H5_PUBLIC_BASE_URL=https://gadm.tkmind.cn
|
||||
H5_PUBLIC_BASE_URL=https://md.tkmind.cn
|
||||
VITE_BASE_PATH=/ops
|
||||
|
||||
# 共享 Memind 主库与主实现
|
||||
@@ -196,7 +196,7 @@ MEMIND_LIB_ROOT=/Users/john/Project/Memind
|
||||
H5_USERS_ROOT=/Users/john/Project/Memind/users
|
||||
|
||||
# 套餐自动同步到生产后台(可选)
|
||||
PLAN_SYNC_TARGET_BASE_URL=https://gadm.tkmind.cn
|
||||
PLAN_SYNC_TARGET_BASE_URL=https://md.tkmind.cn
|
||||
PLAN_SYNC_USERNAME=admin
|
||||
PLAN_SYNC_PASSWORD=<admin-password>
|
||||
PLAN_SYNC_TIMEOUT_MS=10000
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gadm.tkmind.cn → memind_adm(vite preview :5174 + Admin API :8085)
|
||||
# md.tkmind.cn → memind_adm(vite preview :5174 + Admin API :8085)
|
||||
# 生产 nginx 需把 /ops/ 反代到 5174,勿再指向 Memind/ops dev server
|
||||
:8091 {
|
||||
reverse_proxy 127.0.0.1:5174 {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# gadm.tkmind.cn @ 105 nginx → Studio LAN 10.10.0.2(fallback 58.38.22.103)
|
||||
# md.tkmind.cn @ 105 nginx → Studio LAN 10.10.0.2(fallback 58.38.22.103)
|
||||
# memind_adm: preview :5174, Admin API :8085(根路径部署,/ 直达后台)
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name gadm.tkmind.cn;
|
||||
server_name md.tkmind.cn;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
@@ -18,9 +18,9 @@ server {
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name gadm.tkmind.cn;
|
||||
ssl_certificate /etc/letsencrypt/live/gadm.tkmind.cn/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/gadm.tkmind.cn/privkey.pem;
|
||||
server_name md.tkmind.cn;
|
||||
ssl_certificate /etc/letsencrypt/live/md.tkmind.cn/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/md.tkmind.cn/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gadm.tkmind.cn — memind_adm 独立管理端(替换原 Memind /ops Vite dev)
|
||||
# md.tkmind.cn — memind_adm 独立管理端(替换原 Memind /ops Vite dev)
|
||||
#
|
||||
# 前置:100 服务器 remote_restart.sh 已启动
|
||||
# Admin API: 127.0.0.1:8085
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name gadm.tkmind.cn;
|
||||
server_name md.tkmind.cn;
|
||||
|
||||
# ssl_certificate ...;
|
||||
|
||||
|
||||
@@ -4,24 +4,27 @@ set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PORT="${ADM_PORT:-5174}"
|
||||
API_PORT="${ADM_API_PORT:-8085}"
|
||||
NODE_PATH="${LOCAL_NODE_PATH:-/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
|
||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||
LOG="${ROOT}/adm-preview.log"
|
||||
API_LOG="${ROOT}/adm-api.log"
|
||||
PID_FILE="${ROOT}/.adm-preview.pid"
|
||||
API_PID_FILE="${ROOT}/.adm-api.pid"
|
||||
|
||||
export PATH="${NODE_PATH}:${PATH}"
|
||||
command -v node >/dev/null 2>&1 || { echo "❌ 未找到 node" >&2; exit 1; }
|
||||
[[ -x "${NODE_BIN}" ]] || { echo "❌ NODE_BIN 不可执行: ${NODE_BIN}" >&2; exit 1; }
|
||||
NODE_DIR="$(dirname "${NODE_BIN}")"
|
||||
export PATH="${NODE_DIR}:${PATH}"
|
||||
NPM_BIN="${NPM_BIN:-${NODE_DIR}/npm}"
|
||||
[[ -x "${NPM_BIN}" ]] || { echo "❌ NPM_BIN 不可执行: ${NPM_BIN}" >&2; exit 1; }
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
if [[ ! -d node_modules ]]; then
|
||||
echo "==> 安装依赖..."
|
||||
npm install
|
||||
"${NPM_BIN}" install
|
||||
fi
|
||||
|
||||
echo "==> 本机构建..."
|
||||
npm run build
|
||||
"${NPM_BIN}" run build
|
||||
|
||||
echo "==> 停止旧进程 (port ${PORT}, ${API_PORT})..."
|
||||
lsof -ti "TCP:${PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
||||
@@ -32,7 +35,7 @@ lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null
|
||||
rm -f "${PID_FILE}" "${API_PID_FILE}"
|
||||
|
||||
echo "==> 启动本机 Admin API @ 127.0.0.1:${API_PORT}..."
|
||||
nohup node server/index.mjs >>"${API_LOG}" 2>&1 &
|
||||
nohup "${NODE_BIN}" server/index.mjs >>"${API_LOG}" 2>&1 &
|
||||
echo $! >"${API_PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
@@ -43,7 +46,7 @@ if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then
|
||||
fi
|
||||
|
||||
echo "==> 启动本机 vite preview @ 127.0.0.1:${PORT}..."
|
||||
nohup npm run dev:preview -- --host 127.0.0.1 --port "${PORT}" >>"${LOG}" 2>&1 &
|
||||
nohup "${NPM_BIN}" run dev:preview -- --host 127.0.0.1 --port "${PORT}" >>"${LOG}" 2>&1 &
|
||||
echo $! >"${PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
set -a
|
||||
source "${ROOT}/.env"
|
||||
if [[ -f "${ROOT}/.env.colima" ]]; then
|
||||
source "${ROOT}/.env.colima"
|
||||
fi
|
||||
set +a
|
||||
|
||||
exec bash "${ROOT}/scripts/local_restart.sh"
|
||||
+10
-9
@@ -116,6 +116,10 @@ say "打包 Memind 共享模块到 memind-lib"
|
||||
rm -rf "${MEMIND_LIB}"
|
||||
mkdir -p "${MEMIND_LIB}"
|
||||
find "${MEMIND_SRC}" -maxdepth 1 -name '*.mjs' -exec cp -f {} "${MEMIND_LIB}/" \;
|
||||
# 同步技能目录(skills/ 下的 SKILL.md 是技能目录来源)
|
||||
if [[ -d "${MEMIND_SRC}/skills" ]]; then
|
||||
cp -a "${MEMIND_SRC}/skills" "${MEMIND_LIB}/"
|
||||
fi
|
||||
[[ -f "${MEMIND_LIB}/user-auth.mjs" ]] || {
|
||||
echo "memind-lib 缺少 user-auth.mjs" >&2
|
||||
exit 1
|
||||
@@ -225,15 +229,12 @@ done
|
||||
|
||||
say "安装运行依赖"
|
||||
cd "${RELEASE_DIR}"
|
||||
export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
NPM_BIN="$(command -v npm)"
|
||||
elif [[ -x /opt/homebrew/opt/node@24/bin/npm ]]; then
|
||||
NPM_BIN="/opt/homebrew/opt/node@24/bin/npm"
|
||||
else
|
||||
echo "npm not found on remote" >&2
|
||||
exit 1
|
||||
fi
|
||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||
[[ -x "${NODE_BIN}" ]] || { echo "NODE_BIN is not executable: ${NODE_BIN}" >&2; exit 1; }
|
||||
NODE_DIR="$(dirname "${NODE_BIN}")"
|
||||
export PATH="${NODE_DIR}:${PATH}"
|
||||
NPM_BIN="${NPM_BIN:-${NODE_DIR}/npm}"
|
||||
[[ -x "${NPM_BIN}" ]] || { echo "NPM_BIN is not executable: ${NPM_BIN}" >&2; exit 1; }
|
||||
if [[ -f package-lock.json ]]; then
|
||||
"${NPM_BIN}" ci --no-audit --no-fund
|
||||
else
|
||||
|
||||
@@ -4,15 +4,18 @@ set -euo pipefail
|
||||
|
||||
ROOT="${ADM_ROOT:-/Users/john/Project/memind_adm}"
|
||||
PORT="${ADM_PORT:-5174}"
|
||||
NODE_PATH="${LAN_NODE_PATH:-/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
|
||||
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
|
||||
API_PORT="${ADM_API_PORT:-8085}"
|
||||
LOG="${ROOT}/adm-preview.log"
|
||||
API_LOG="${ROOT}/adm-api.log"
|
||||
PID_FILE="${ROOT}/.adm-preview.pid"
|
||||
API_PID_FILE="${ROOT}/.adm-api.pid"
|
||||
|
||||
export PATH="${NODE_PATH}:${PATH}"
|
||||
command -v node >/dev/null 2>&1 || { echo "❌ 未找到 node" >&2; exit 1; }
|
||||
[[ -x "${NODE_BIN}" ]] || { echo "❌ NODE_BIN 不可执行: ${NODE_BIN}" >&2; exit 1; }
|
||||
NODE_DIR="$(dirname "${NODE_BIN}")"
|
||||
export PATH="${NODE_DIR}:${PATH}"
|
||||
NPM_BIN="${NPM_BIN:-${NODE_DIR}/npm}"
|
||||
[[ -x "${NPM_BIN}" ]] || { echo "❌ NPM_BIN 不可执行: ${NPM_BIN}" >&2; exit 1; }
|
||||
|
||||
cd "${ROOT}"
|
||||
|
||||
@@ -41,7 +44,7 @@ fi
|
||||
|
||||
if [[ ! -d node_modules ]]; then
|
||||
echo "==> 安装依赖..."
|
||||
npm install
|
||||
"${NPM_BIN}" install
|
||||
fi
|
||||
|
||||
echo "==> 停止旧进程 (port ${PORT}, ${API_PORT})..."
|
||||
@@ -59,7 +62,7 @@ lsof -ti "TCP:${API_PORT}" -sTCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null
|
||||
sleep 1
|
||||
|
||||
echo "==> 启动 Admin API @ ${API_HOST}:${API_PORT}..."
|
||||
nohup node server/index.mjs >>"${API_LOG}" 2>&1 &
|
||||
nohup "${NODE_BIN}" server/index.mjs >>"${API_LOG}" 2>&1 &
|
||||
echo $! >"${API_PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
@@ -70,7 +73,7 @@ if ! curl -sf "http://127.0.0.1:${API_PORT}/health" >/dev/null; then
|
||||
fi
|
||||
|
||||
echo "==> 启动 vite preview @ ${WEB_HOST}:${PORT} (base=${BASE_PATH})..."
|
||||
nohup npx vite preview --config scripts/vite-preview.config.mjs >>"${LOG}" 2>&1 &
|
||||
nohup "${NPM_BIN}" exec vite preview --config scripts/vite-preview.config.mjs >>"${LOG}" 2>&1 &
|
||||
echo $! >"${PID_FILE}"
|
||||
sleep 3
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig(({ mode }) => {
|
||||
host: env.ADM_WEB_HOST ?? env.ADM_API_HOST ?? '127.0.0.1',
|
||||
port: Number(env.ADM_PORT ?? 5174),
|
||||
strictPort: true,
|
||||
allowedHosts: ['gadm.tkmind.cn', 'localhost', '127.0.0.1', '10.10.0.2', '58.38.22.103'],
|
||||
allowedHosts: ['md.tkmind.cn', 'gadm.tkmind.cn', 'localhost', '127.0.0.1', '10.10.0.2', '58.38.22.103'],
|
||||
proxy: {
|
||||
'/api': backend,
|
||||
'/auth': backend,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
/**
|
||||
* GoosedExecutor — runs a skill via a one-shot goosed session.
|
||||
* GoosedExecutor — runs a skill via a goosed session.
|
||||
*
|
||||
* Session lifecycle:
|
||||
* 1. POST /agent/start { messages: [{ role: 'user', content: prompt }] }
|
||||
* 2. GET /sessions/:id/events (SSE) → collect until Finish event or timeout
|
||||
* 3. DELETE /sessions/:id (best-effort cleanup)
|
||||
* 1. POST /agent/start { working_dir }
|
||||
* 2. POST /sessions/:id/reply { request_id: UUID, user_message: {...} }
|
||||
* 3. GET /sessions/:id/events (SSE) → collect until Finish event or timeout
|
||||
* 4. DELETE /sessions/:id (best-effort cleanup)
|
||||
*
|
||||
* Dual-node: pickGoosedUrl() round-robins across configured nodes.
|
||||
* Protocol: HTTPS with self-signed cert (rejectUnauthorized: false).
|
||||
*/
|
||||
|
||||
import https from 'node:https';
|
||||
import crypto from 'node:crypto';
|
||||
import { goosedFetch, pickGoosedUrl } from '../goosed-client.mjs';
|
||||
|
||||
const SESSION_TIMEOUT_MS = Number(process.env.SKILL_GOOSED_TIMEOUT_MS ?? 120_000);
|
||||
@@ -18,19 +20,14 @@ const tlsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||
|
||||
export async function invokeGoosedSkill(skill, input) {
|
||||
const node = pickGoosedUrl();
|
||||
const prompt = buildPrompt(skill, input);
|
||||
|
||||
const systemPrompt = buildSystemPrompt(skill, input);
|
||||
const userPrompt = buildUserPrompt(skill, input);
|
||||
|
||||
// 1. Start session
|
||||
// 1. Start session (no messages yet)
|
||||
const startRes = await goosedFetch('/agent/start', {
|
||||
method: 'POST',
|
||||
node,
|
||||
body: {
|
||||
version: '1.0.0',
|
||||
session_id: null,
|
||||
...(systemPrompt ? { system_prompt_override: systemPrompt } : {}),
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
working_dir: process.env.GOOSE_SKILL_WORKING_DIR ?? '/tmp',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,12 +38,42 @@ export async function invokeGoosedSkill(skill, input) {
|
||||
);
|
||||
}
|
||||
|
||||
const sessionId = startRes.body?.session_id ?? startRes.body?.id;
|
||||
const sessionId = startRes.body?.id ?? startRes.body?.session_id;
|
||||
if (!sessionId) {
|
||||
throw new Error(`goosed did not return a session_id: ${JSON.stringify(startRes.body)}`);
|
||||
throw new Error(`goosed did not return a session id: ${JSON.stringify(startRes.body)}`);
|
||||
}
|
||||
|
||||
// 2. Stream events and collect result
|
||||
// 1b. Set the LLM provider on the session
|
||||
const goosedProvider = process.env.GOOSE_PROVIDER ?? process.env.GOOSED_PROVIDER ?? 'custom_deepseek';
|
||||
const goosedModel = process.env.GOOSE_MODEL ?? process.env.GOOSED_MODEL;
|
||||
const providerBody = { session_id: sessionId, provider: goosedProvider };
|
||||
if (goosedModel) providerBody.model = goosedModel;
|
||||
await goosedFetch('/agent/update_provider', { method: 'POST', node, body: providerBody });
|
||||
|
||||
// 2. Send the prompt via reply
|
||||
const requestId = crypto.randomUUID();
|
||||
const replyRes = await goosedFetch(`/sessions/${sessionId}/reply`, {
|
||||
method: 'POST',
|
||||
node,
|
||||
body: {
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
role: 'user',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
metadata: { userVisible: true, agentVisible: true },
|
||||
content: [{ type: 'text', text: prompt }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!replyRes.ok) {
|
||||
throw Object.assign(
|
||||
new Error(`goosed /sessions/${sessionId}/reply failed (${replyRes.status})`),
|
||||
{ status: replyRes.status, upstream: replyRes.body },
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Stream SSE events and collect result
|
||||
try {
|
||||
return await collectSessionResult(node, sessionId);
|
||||
} finally {
|
||||
@@ -54,14 +81,22 @@ export async function invokeGoosedSkill(skill, input) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildSystemPrompt(skill, input) {
|
||||
if (skill.runtime?.config?.system_prompt) return skill.runtime.config.system_prompt;
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildUserPrompt(skill, input) {
|
||||
function buildPrompt(skill, input) {
|
||||
if (input.design_md_content) {
|
||||
return `You are a UI generation assistant. Follow this design system exactly:\n\n${input.design_md_content}\n\nTask: ${input.task ?? input.prompt ?? ''}`;
|
||||
return `You are a UI generation assistant. Your output must be a complete, self-contained HTML page — not a component, not a snippet, not a code block with backticks. Output raw HTML only, starting with <!DOCTYPE html>.
|
||||
|
||||
Apply the following design system exactly. Match the colors, typography, spacing, border styles, shadows, and interaction patterns described:
|
||||
|
||||
${input.design_md_content}
|
||||
|
||||
Task: ${input.task ?? input.prompt ?? ''}
|
||||
|
||||
Requirements:
|
||||
- Output a single complete HTML file with <html>, <head>, <body> and all inline <style> — no external stylesheets or JS frameworks
|
||||
- The page should feel like a real product page from this brand, not a generic template
|
||||
- Use the exact color values, font sizes, border-radius, shadow formulas from the design system above
|
||||
- Include realistic placeholder content relevant to the task
|
||||
- The page must be visually complete and ready to render in a browser`;
|
||||
}
|
||||
return input.task ?? input.prompt ?? JSON.stringify(input);
|
||||
}
|
||||
@@ -80,13 +115,10 @@ function collectSessionResult(node, sessionId) {
|
||||
path: url.pathname,
|
||||
method: 'GET',
|
||||
headers: { 'X-Secret-Key': secret, Accept: 'text/event-stream' },
|
||||
agent: url.protocol === 'https:' ? tlsAgent : undefined,
|
||||
rejectUnauthorized: false,
|
||||
agent: tlsAgent,
|
||||
};
|
||||
|
||||
const proto = url.protocol === 'https:' ? https : (() => { throw new Error('http not supported for goosed'); })();
|
||||
|
||||
const req = proto.request(options, (res) => {
|
||||
const req = https.request(options, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
res.resume();
|
||||
return reject(new Error(`SSE stream returned ${res.statusCode}`));
|
||||
@@ -144,14 +176,12 @@ function collectSessionResult(node, sessionId) {
|
||||
}
|
||||
|
||||
function extractText(event) {
|
||||
// Handle goosed SSE event shapes
|
||||
if (event?.type === 'Message' && event?.message?.role === 'assistant') {
|
||||
return (event.message.content ?? [])
|
||||
.filter((c) => c.type === 'text')
|
||||
.map((c) => c.text)
|
||||
.join('');
|
||||
}
|
||||
// Simpler delta format
|
||||
if (event?.delta?.text) return event.delta.text;
|
||||
if (typeof event?.content === 'string') return event.content;
|
||||
return '';
|
||||
|
||||
@@ -46,28 +46,46 @@ export function goosedUrls() {
|
||||
/**
|
||||
* Make a JSON request to a goosed node.
|
||||
* Returns { ok, status, body } — never throws on HTTP errors, only on network failure.
|
||||
* Uses https.request directly so rejectUnauthorized: false works for self-signed certs.
|
||||
*/
|
||||
export async function goosedFetch(path, { method = 'GET', body, node } = {}) {
|
||||
export function goosedFetch(path, { method = 'GET', body, node } = {}) {
|
||||
const base = node ?? pickGoosedUrl();
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY ??
|
||||
const secret =
|
||||
process.env.GOOSE_SERVER__SECRET_KEY ??
|
||||
process.env.TKMIND_SERVER__SECRET_KEY ??
|
||||
'local-dev-secret';
|
||||
|
||||
const url = `${base}${path}`;
|
||||
const init = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': secret,
|
||||
},
|
||||
agent: tlsAgent,
|
||||
};
|
||||
if (body !== undefined) init.body = JSON.stringify(body);
|
||||
const url = new URL(`${base}${path}`);
|
||||
const payload = body !== undefined ? JSON.stringify(body) : null;
|
||||
|
||||
const res = await fetch(url, init);
|
||||
let json = null;
|
||||
const text = await res.text();
|
||||
try { json = JSON.parse(text); } catch { json = { raw: text }; }
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': secret,
|
||||
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
||||
},
|
||||
agent: tlsAgent,
|
||||
};
|
||||
|
||||
return { ok: res.ok, status: res.status, body: json };
|
||||
const req = https.request(options, (res) => {
|
||||
let text = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => { text += chunk; });
|
||||
res.on('end', () => {
|
||||
let json = null;
|
||||
try { json = JSON.parse(text); } catch { json = { raw: text }; }
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, body: json });
|
||||
});
|
||||
res.on('error', reject);
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (payload) req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function ensureSkillSchema(pool) {
|
||||
CREATE TABLE IF NOT EXISTS adm_skill_invocations (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
skill_id INT UNSIGNED NOT NULL,
|
||||
caller_id INT UNSIGNED NULL,
|
||||
caller_id VARCHAR(64) NULL,
|
||||
caller_type VARCHAR(32) NOT NULL DEFAULT 'unknown',
|
||||
input_snapshot JSON NULL,
|
||||
status ENUM('pending','running','success','failed','blocked_by_flag')
|
||||
|
||||
@@ -1,6 +1,93 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
listBackendSkills,
|
||||
setBackendSkillEnabled,
|
||||
type BackendSkill,
|
||||
} from '../../api/client';
|
||||
import { SkillSettings } from '../../components/SkillSettings';
|
||||
import { useAdminUsers } from '../hooks/useAdminUsers';
|
||||
|
||||
function BackendSkillCard({
|
||||
skill,
|
||||
onToggle,
|
||||
}: {
|
||||
skill: BackendSkill;
|
||||
onToggle: (slug: string, enabled: boolean) => Promise<void>;
|
||||
}) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const handleToggle = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await onToggle(skill.slug, !skill.enabled);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`backend-skill-card${skill.enabled ? '' : ' disabled'}`}>
|
||||
<div className="backend-skill-info">
|
||||
<div className="backend-skill-name">
|
||||
{skill.name}
|
||||
<span className="backend-skill-slug"> · {skill.slug}</span>
|
||||
</div>
|
||||
{skill.meta?.description && (
|
||||
<div className="backend-skill-desc">{skill.meta.description}</div>
|
||||
)}
|
||||
{!skill.enabled && (
|
||||
<div className="backend-skill-reason">已全局禁用</div>
|
||||
)}
|
||||
<div className="backend-skill-meta">
|
||||
<span className="backend-skill-tag">{skill.type}</span>
|
||||
{skill.meta?.tags?.map((t) => (
|
||||
<span key={t} className="backend-skill-tag">{t}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<label className="toggle-switch" title={skill.enabled ? '点击禁用' : '点击启用'}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={skill.enabled}
|
||||
disabled={busy}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BackendSkillsPanel() {
|
||||
const [skills, setSkills] = useState<BackendSkill[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
listBackendSkills()
|
||||
.then(setSkills)
|
||||
.catch((e) => setError(e.message ?? '加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleToggle = useCallback(async (slug: string, enabled: boolean) => {
|
||||
await setBackendSkillEnabled(slug, enabled);
|
||||
setSkills((prev) => prev.map((s) => (s.slug === slug ? { ...s, enabled } : s)));
|
||||
}, []);
|
||||
|
||||
if (loading) return <p className="muted">加载技能列表…</p>;
|
||||
if (error) return <p className="banner banner-error">{error}</p>;
|
||||
if (!skills.length) return <p className="muted">暂无后台技能</p>;
|
||||
|
||||
return (
|
||||
<div className="backend-skill-list">
|
||||
{skills.map((s) => (
|
||||
<BackendSkillCard key={s.slug} skill={s} onToggle={handleToggle} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkillsPage() {
|
||||
const { users, loading, error } = useAdminUsers();
|
||||
|
||||
@@ -10,8 +97,20 @@ export function SkillsPage() {
|
||||
<h2>技能配置</h2>
|
||||
<p className="muted">管理平台技能与用户 MindSpace 安装</p>
|
||||
</div>
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{loading ? <p className="muted">加载用户列表…</p> : <SkillSettings users={users} />}
|
||||
|
||||
<section className="admin-section">
|
||||
<h3 className="admin-section-title">后台技能市场</h3>
|
||||
<p className="muted" style={{ marginBottom: 12 }}>
|
||||
控制 AI 生成技能的全局开关,关闭后所有用户均无法调用。
|
||||
</p>
|
||||
<BackendSkillsPanel />
|
||||
</section>
|
||||
|
||||
<section className="admin-section" style={{ marginTop: 32 }}>
|
||||
<h3 className="admin-section-title">用户技能权限</h3>
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{loading ? <p className="muted">加载用户列表…</p> : <SkillSettings users={users} />}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -598,6 +598,39 @@ export async function clearUserSkillOverrides(userId: string): Promise<void> {
|
||||
await portalFetch(`/admin-api/users/${userId}/skills`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// ── Backend Skills (Skill Marketplace) ────────────────
|
||||
|
||||
export interface BackendSkill {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
type: string;
|
||||
status: string;
|
||||
enabled: boolean;
|
||||
rollout_pct: number;
|
||||
meta: {
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
source?: string;
|
||||
} | null;
|
||||
publisher_id: string | null;
|
||||
}
|
||||
|
||||
export async function listBackendSkills(): Promise<BackendSkill[]> {
|
||||
const result = await portalFetch<{ skills: BackendSkill[] }>('/admin-api/skills/');
|
||||
return result.skills ?? [];
|
||||
}
|
||||
|
||||
export async function setBackendSkillEnabled(
|
||||
slug: string,
|
||||
enabled: boolean,
|
||||
): Promise<{ ok: boolean; slug: string; enabled: boolean }> {
|
||||
return portalFetch(`/admin-api/skills/${slug}/enabled`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
}
|
||||
|
||||
// ── LLM Providers ─────────────────────────────────────
|
||||
|
||||
export async function listLlmProviderCatalog(): Promise<LlmProviderDefinition[]> {
|
||||
|
||||
+119
@@ -1660,3 +1660,122 @@ body,
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ── Admin section grouping ── */
|
||||
.admin-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.admin-section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #111);
|
||||
margin: 0 0 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border, #e5e5e5);
|
||||
}
|
||||
|
||||
/* ── Backend Skill Cards ── */
|
||||
.backend-skill-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-width: 680px;
|
||||
}
|
||||
.backend-skill-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border, #e5e5e5);
|
||||
border-radius: 8px;
|
||||
background: var(--surface, #fff);
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.backend-skill-card.disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.backend-skill-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.backend-skill-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #111);
|
||||
}
|
||||
.backend-skill-slug {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #888);
|
||||
}
|
||||
.backend-skill-desc {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary, #555);
|
||||
margin-top: 3px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.backend-skill-reason {
|
||||
font-size: 12px;
|
||||
color: var(--error, #c00);
|
||||
margin-top: 4px;
|
||||
}
|
||||
.backend-skill-meta {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.backend-skill-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
background: var(--surface-alt, #f3f3f3);
|
||||
color: var(--text-muted, #888);
|
||||
}
|
||||
|
||||
/* ── Toggle Switch (reusable) ── */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
}
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 22px;
|
||||
background: var(--border, #ccc);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.toggle-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: transform 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.2);
|
||||
}
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background: var(--accent, #1a73e8);
|
||||
}
|
||||
.toggle-switch input:checked + .toggle-slider::before {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
.toggle-switch input:disabled + .toggle-slider {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ export default defineConfig(({ mode }) => {
|
||||
host: true,
|
||||
port: 5174,
|
||||
strictPort: true,
|
||||
allowedHosts: ['gadm.tkmind.cn', 'localhost', '127.0.0.1'],
|
||||
allowedHosts: ['md.tkmind.cn', 'gadm.tkmind.cn', 'localhost', '127.0.0.1'],
|
||||
proxy: {
|
||||
'/api': backendTarget,
|
||||
'/auth': backendTarget,
|
||||
@@ -25,7 +25,7 @@ export default defineConfig(({ mode }) => {
|
||||
host: '127.0.0.1',
|
||||
port: 5174,
|
||||
strictPort: true,
|
||||
allowedHosts: ['gadm.tkmind.cn', 'localhost', '127.0.0.1'],
|
||||
allowedHosts: ['md.tkmind.cn', 'gadm.tkmind.cn', 'localhost', '127.0.0.1'],
|
||||
proxy: {
|
||||
'/api': backendTarget,
|
||||
'/auth': backendTarget,
|
||||
|
||||
Reference in New Issue
Block a user