V3.5 精细化运营升级:用量统计/接口库/团队/对话/工作目录/流式超时

1. 精细化统计:cost_records 新增 calls/cached_tokens/latency_ms/first_token_ms;
   用量明细报表(项目×智能体矩阵) + 成本报表细化(输入/输出/缓存命中/调用次数)
2. 从参考项目中新建:内置3个测试项目(文案/Python/调研),一键复制目标+任务
3. 大模型接口库(llm_endpoints):专门配置接口(地址/密钥/模型/定价),
   计费支持按token(逐模型)与按调用次数;创建AI Worker直接选用;
   AI Worker团队(worker_teams):打包Worker,对话/建项目可直接选团队
4. 对话导航融合仪表盘:可选大模型/AI Worker/团队,默认主力AI Worker(可设),SSE流式
5. 系统工作目录:默认data/workspace可改绝对路径;项目与多Agent协作均在其下
   建唯一工作目录;手动输入目录已存在则列出信息并需手动确认
6. 任务执行超时改为流式单token返回超时+首字延迟超时,设置页可配;
   所有模型输出SSE按token接收
This commit is contained in:
2026-09-05 15:27:43 +08:00
parent 339dd89ba6
commit c56745a8ab
13 changed files with 2231 additions and 197 deletions
+223
View File
@@ -390,6 +390,66 @@ CREATE INDEX IF NOT EXISTS idx_cost_project ON cost_records(project_id);
CREATE INDEX IF NOT EXISTS idx_docs_project ON documents(project_id);
CREATE INDEX IF NOT EXISTS idx_chunks_doc ON doc_chunks(document_id);
CREATE INDEX IF NOT EXISTS idx_alerts_read ON alerts(read);
-- ===================================================================
-- V3.5 表结构:大模型接口库 / AI Worker 团队 / 对话 / 精细化计量
-- ===================================================================
CREATE TABLE IF NOT EXISTS llm_endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
provider TEXT DEFAULT 'custom', -- doubao/deepseek/autodl/qwen/openai/vllm/custom
base_url TEXT DEFAULT '',
api_key TEXT DEFAULT '',
models TEXT DEFAULT '[]', -- JSON: 可用模型名列表
pricing TEXT DEFAULT '{}', -- JSON: {model: {input: 元/1M, output: 元/1M}}
input_price REAL DEFAULT 0, -- 兜底输入价(元/1M tokens
output_price REAL DEFAULT 0, -- 兜底输出价(元/1M tokens
price_per_call REAL DEFAULT 0, -- 按调用次数计费单价(元/次)
billing TEXT DEFAULT 'token', -- token=按token数计费 / call=按调用次数计费
description TEXT DEFAULT '',
status TEXT DEFAULT 'enabled', -- enabled/disabled
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS worker_teams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT DEFAULT '',
worker_ids TEXT DEFAULT '[]', -- JSON: worker id 列表
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS chat_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT DEFAULT '',
target_type TEXT DEFAULT 'worker', -- model=大模型接口 / worker=AI Worker / team=团队
target_id INTEGER DEFAULT 0,
model TEXT DEFAULT '', -- target_type=model 时选定的模型名
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
role TEXT DEFAULT 'user', -- user/assistant
content TEXT DEFAULT '',
model TEXT DEFAULT '',
worker_id INTEGER,
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
cached_tokens INTEGER DEFAULT 0,
cost REAL DEFAULT 0,
latency_ms INTEGER DEFAULT 0,
first_token_ms INTEGER DEFAULT 0,
error TEXT DEFAULT '',
created_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_chat_msg_session ON chat_messages(session_id);
CREATE INDEX IF NOT EXISTS idx_cost_worker ON cost_records(worker_id);
"""
# ---------------------------------------------------------------------------
@@ -468,6 +528,86 @@ def _migrate():
for r in conn.execute("SELECT id, workspace_dir FROM projects WHERE workspace_dir IS NULL OR workspace_dir=''"):
conn.execute('UPDATE projects SET workspace_dir=? WHERE id=?',
('project_%d' % r['id'], r['id']))
# ================= V3.5 迁移:精细化计量 / 接口库 / 团队 / 对话 =================
ccols = {r['name'] for r in conn.execute('PRAGMA table_info(cost_records)')}
for col, ddl in (
('calls', 'ALTER TABLE cost_records ADD COLUMN calls INTEGER DEFAULT 1'),
('cached_tokens', 'ALTER TABLE cost_records ADD COLUMN cached_tokens INTEGER DEFAULT 0'),
('latency_ms', 'ALTER TABLE cost_records ADD COLUMN latency_ms INTEGER DEFAULT 0'),
('first_token_ms', 'ALTER TABLE cost_records ADD COLUMN first_token_ms INTEGER DEFAULT 0'),
):
if col not in ccols:
conn.execute(ddl)
wcols = {r['name'] for r in conn.execute('PRAGMA table_info(workers)')}
for col, ddl in (
('endpoint_id', 'ALTER TABLE workers ADD COLUMN endpoint_id INTEGER'),
('is_main', 'ALTER TABLE workers ADD COLUMN is_main INTEGER DEFAULT 0'),
):
if col not in wcols:
conn.execute(ddl)
pcols3 = {r['name'] for r in conn.execute('PRAGMA table_info(projects)')}
if 'is_reference' not in pcols3:
conn.execute('ALTER TABLE projects ADD COLUMN is_reference INTEGER DEFAULT 0')
acols = {r['name'] for r in conn.execute('PRAGMA table_info(agent_runs)')}
if 'workspace_dir' not in acols:
conn.execute("ALTER TABLE agent_runs ADD COLUMN workspace_dir TEXT DEFAULT ''")
scols = {r['name'] for r in conn.execute('PRAGMA table_info(chat_sessions)')}
if 'model' not in scols:
conn.execute("ALTER TABLE chat_sessions ADD COLUMN model TEXT DEFAULT ''")
# V3.5 新表(幂等)
conn.execute('''CREATE TABLE IF NOT EXISTS llm_endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
provider TEXT DEFAULT 'custom',
base_url TEXT DEFAULT '',
api_key TEXT DEFAULT '',
models TEXT DEFAULT '[]',
pricing TEXT DEFAULT '{}',
input_price REAL DEFAULT 0,
output_price REAL DEFAULT 0,
price_per_call REAL DEFAULT 0,
billing TEXT DEFAULT 'token',
description TEXT DEFAULT '',
status TEXT DEFAULT 'enabled',
created_at INTEGER,
updated_at INTEGER
)''')
conn.execute('''CREATE TABLE IF NOT EXISTS worker_teams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT DEFAULT '',
worker_ids TEXT DEFAULT '[]',
created_at INTEGER,
updated_at INTEGER
)''')
conn.execute('''CREATE TABLE IF NOT EXISTS chat_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT DEFAULT '',
target_type TEXT DEFAULT 'worker',
target_id INTEGER DEFAULT 0,
model TEXT DEFAULT '',
created_at INTEGER,
updated_at INTEGER
)''')
conn.execute('''CREATE TABLE IF NOT EXISTS chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
role TEXT DEFAULT 'user',
content TEXT DEFAULT '',
model TEXT DEFAULT '',
worker_id INTEGER,
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
cached_tokens INTEGER DEFAULT 0,
cost REAL DEFAULT 0,
latency_ms INTEGER DEFAULT 0,
first_token_ms INTEGER DEFAULT 0,
error TEXT DEFAULT '',
created_at INTEGER
)''')
conn.execute('CREATE INDEX IF NOT EXISTS idx_chat_msg_session ON chat_messages(session_id)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_cost_worker ON cost_records(worker_id)')
conn.commit()
conn.close()
@@ -524,6 +664,8 @@ def init_db():
_migrate()
migrate_v2()
seed_builtin_roles()
seed_endpoints()
link_workers_endpoints()
# V3.1 内置角色权限点定义(admin 为特殊值 ALL,表示全部权限)
@@ -573,6 +715,87 @@ def recover_stale_runs():
return (n1 or 0, n2 or 0, n3 or 0)
# ---------------------------------------------------------------------------
# V3.5 大模型接口库:从 config.PROVIDERS/MODEL_PRICING 自动导入 + 存量 Worker 关联
# ---------------------------------------------------------------------------
def seed_endpoints():
"""幂等:首次启动把 config.PROVIDERS 导入为大模型接口库(含逐模型定价),
便于在页面上统一管理与配置价格(按 token / 按调用次数)。"""
c = q('SELECT COUNT(*) c FROM llm_endpoints')[0]['c']
if c > 0:
return
import config as cfg
prefix = {'doubao': 'doubao', 'deepseek': 'deepseek', 'openai': 'gpt',
'qwen': 'qwen', 'vllm': '', 'autodl': 'qwen3'}
ts = now()
for pid, p in cfg.PROVIDERS.items():
models = [m for m in cfg.MODEL_PRICING if m.startswith(prefix.get(pid, '__none__'))]
pricing = {m: cfg.MODEL_PRICING[m] for m in models}
fp = cfg.MODEL_PRICING.get(models[0]) if models else cfg.DEFAULT_PRICE
w('INSERT INTO llm_endpoints (name, provider, base_url, api_key, models, pricing, '
'input_price, output_price, price_per_call, billing, description, status, created_at, updated_at) '
'VALUES (?,?,?,?,?,?,?,?,0,?,?,?,?,?)',
(p['name'], pid, p['base_url'], p['api_key'], json.dumps(models), json.dumps(pricing),
fp.get('input', 2.0), fp.get('output', 8.0), 'token',
f'由系统配置自动导入({pid}', 'enabled', ts, ts))
def link_workers_endpoints():
"""存量 Workerprovider 匹配的接口库自动关联 endpoint_id(统一计价与鉴权)"""
conn = get_conn()
try:
eps = {r['provider']: r['id'] for r in conn.execute('SELECT id, provider FROM llm_endpoints')}
for r in conn.execute('SELECT id, provider FROM workers WHERE endpoint_id IS NULL OR endpoint_id=0'):
eid = eps.get(r['provider'])
if eid:
conn.execute('UPDATE workers SET endpoint_id=? WHERE id=?', (eid, r['id']))
conn.commit()
finally:
conn.close()
def main_worker_id():
"""主力 AI Worker id:优先取设置 main_worker_id 指向的启用 Worker,否则首个启用 Worker"""
mid = get_setting('main_worker_id', '')
if mid:
r = q('SELECT id FROM workers WHERE id=? AND status="enabled"', (int(mid),), one=True)
if r:
return r['id']
r = q('SELECT id FROM workers WHERE status="enabled" ORDER BY id', one=True)
return r['id'] if r else None
def set_main_worker(worker_id):
"""设置主力 AI Worker:先清除其它 is_main,再标记目标"""
conn = get_conn()
try:
conn.execute('UPDATE workers SET is_main=0 WHERE is_main=1')
conn.execute('UPDATE workers SET is_main=1 WHERE id=?', (int(worker_id),))
conn.commit()
finally:
conn.close()
set_setting('main_worker_id', int(worker_id))
def get_llm_timeouts():
"""读取超时配置:token_timeout(单token返回超时) / first_token_timeout(首字延迟超时) / request_timeout(整体兜底)。
单位秒,0/空 回退 config 默认。"""
import config as cfg
try:
tk = float(get_setting('token_timeout', '') or 0) or cfg.TOKEN_TIMEOUT
except Exception:
tk = cfg.TOKEN_TIMEOUT
try:
fk = float(get_setting('first_token_timeout', '') or 0) or cfg.FIRST_TOKEN_TIMEOUT
except Exception:
fk = cfg.FIRST_TOKEN_TIMEOUT
try:
rk = float(get_setting('request_timeout', '') or 0) or cfg.TASK_TIMEOUT
except Exception:
rk = cfg.TASK_TIMEOUT
return max(3.0, tk), max(3.0, fk), max(10.0, rk)
def q(sql, args=(), one=False):
"""查询"""
conn = get_conn()