init(baseline): import AIOS V1R1C0 multiuser gitops custom llm baseline
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
.env
|
||||
*.secret
|
||||
*.key
|
||||
*.pem
|
||||
*.log
|
||||
__pycache__/
|
||||
node_modules/
|
||||
.venv/
|
||||
.DS_Store
|
||||
runtime/tmp/
|
||||
runtime/cache/
|
||||
logs/
|
||||
@@ -0,0 +1,94 @@
|
||||
e4a1f86ef92ec1d3ff724eb75cf9c01758c1cc997e1afadc1c8b561bce5b813a FULL_PACKAGE_MANIFEST.json
|
||||
c59c0288d4d999fbd79a2f275d4fb8c369029cb8bb721f95408308cc51eaa3a2 README.md
|
||||
1095953d5764d9c72f02e84b1d525e265d3a67a5bdb451ab9ef51a8e8b3032b8 VERSION
|
||||
ee6e4fc6bc98a590fbc3585b62707e2562115e5a3f722e69a7c464d966bdc458 active-runtime/app/aios_gitops_model_api.py
|
||||
0ffadf6ee1f297842bc44c6e753010fd2b7b02b1f5fb0d7e0676b7bec898db08 active-runtime/app/aios_v5_webops_app.py
|
||||
0ffadf6ee1f297842bc44c6e753010fd2b7b02b1f5fb0d7e0676b7bec898db08 active-runtime/app/aios_webops_app.py
|
||||
0eb12e0161371464605577707c84df413b555277933f9eb4488ab38032ff699b active-runtime/migrations/001_webops_core_sqlite.sql
|
||||
6d61da88e7d6b208e7eee7195b521dc47cb68587f91987d7a0dd1c8801ef168d active-runtime/migrations/002_seed_owner_admin_user.sql
|
||||
fcd407fca66d7100242d9231f315ce248dde4c5b6a0679eca923f6fe0d0188eb active-runtime/migrations/003_v5_digital_life_core.sql
|
||||
a2c0c7699e1eb46cfdeabfc905cb79b71ed14fb6bcf81ecc99ec47975cf05767 active-runtime/migrations/004_gitops_custom_llm_runtime.sql
|
||||
ee6e4fc6bc98a590fbc3585b62707e2562115e5a3f722e69a7c464d966bdc458 adapters/gitops/aios_gitops_model_api.py
|
||||
6ed116053cc4a5b0e9c597880bb3ab4494ae46e92dffbe78842e39bf31ff56a9 adapters/hermes/hermes_gitops_nl_router.py
|
||||
013813b58d7844f3860d6f9bb484f84f92487652b573976c474bb35c91df8ae8 adapters/openclaw/openclaw_gitops_commands.py
|
||||
7a34ccdfdcdc132fe5daf01cd1acd9d9b7adbd0429d575b65e0771962ccedc62 adapters/webops/gitops_model_menu_fragment.html
|
||||
86b0cb6ee3192565b080672e5305dabba1283486442e630c71244ebac4910916 config/digital_life.yaml
|
||||
a3872f24e4cd64288758846ecd0b374db4aa41111feec3b17c41438136d6d5d0 config/domains.env
|
||||
7a6f82cbddfc5c74bd37229e4f37fe4e73430ae585b4846abd1a80d852f2fed0 config/gitops_short_links.yaml
|
||||
4139d915bdd733171734d5d5d3411e6fc4f8f202c49af4321d706da176da690b config/model_registry.yaml
|
||||
8d6202d3da60366c96a208f5e5eed1f09fd36d122f4cabdc038c21ce2e72ea16 config/npm_custom_locations.yaml
|
||||
868b34f7986871f6ea1c3d777751a53ed33c84160c436626bceecac73a08e61a config/roles.yaml
|
||||
18b67b3ea729a37f79265cf5782760f5fb3c575218faf0463835a885f785cb38 config/runtime.yaml
|
||||
4743d3fd04132e756e9493fefbaebcc532be7e8ece3c284f5cf9de8d4c536a06 config/runtime_selector.yaml
|
||||
b17620786dc1a412a8285e5c28a6056eb74fd0b5fafcb625a8bb56cf8830d4c4 config/uat_cases.yaml
|
||||
06ea7f9153552e92cb631661d775926e08a27b7f6cbb4e57cbc4484b057afd01 config/webops_pages.yaml
|
||||
d06b69e0fe2311caaad19d63a6c497ef5447dfff14c3c0271475ea771e3fbedc delta-fix/apply_delta_fix_only.sh
|
||||
79d435b2915f73fed5047c3ee3973a6b11f6829d88687a541286c40ad4166491 docs/00_PACKAGE_SCOPE_CN.md
|
||||
55afed17d6f67ae9d6eac88b2522a7e7065dfedc1f59f2db7f2f637d06645f61 docs/01_ARCHITECTURE_FULL_REFRESH_CN.md
|
||||
69aea6c069594bf41d715ff5dcf247fdeab7c6bca3a216abbf0a5e0a812e1038 docs/01_ARCHITECTURE_V5_DIGITAL_LIFE_CN.md
|
||||
158c6cb2b14b6af9d63d23706eef28439d2d02cd6b0d28ed0dc603de8ed1915b docs/02_USER_REQUIREMENTS_OWNER_ADMIN_USER_CN.md
|
||||
1989c0c6228d1db3f1d9c95de8587ae08f9dcea688f0d0c6056c72e5ab45fc36 docs/03_CODEX_OPENCLAW_DEPLOYMENT_CN.md
|
||||
5b9a11da9ccdb2410c8e648923fb713f918887579a7cb51d76100cc3d3f02015 docs/03_CODEX_OPENCLAW_V5_DEPLOYMENT_CN.md
|
||||
d8062c0477f16396097b6c75b3b96da1c1ee70dadd4df9568abee6122369c95a docs/04_DELTA_FIX_ONLY_RUNBOOK_CN.md
|
||||
6a184451ee5e01142ab585d0f4a2aec778bb9bb0dae7e9d95a3fd3692dbcbb9c docs/04_WEBOPS_UI_V5_FULL_MATRIX_CN.md
|
||||
914e43000d3354b66c97e8db44812f4f6f156d78fe287208613dba44c4e51533 docs/05_WEBOPS_UI_FUNCTION_MATRIX_CN.md
|
||||
18663aeba961ac3ce300032d284e99958fd117cda1aec6b9342dc0b0cd9cd6c7 docs/06_ACCEPTANCE_UAT_V5_CN.md
|
||||
c996535b9f1291e05d5e278b61b221cd4383c123aebbc1222df22a48076fe063 docs/06_UAT_ACCEPTANCE_FULL_CN.md
|
||||
744f10503feccb553e4691ebc75c6a5e229e4e8eff921dd6c7f46b0cb0343710 docs/07_CHATGPT_CODEX_COLLABORATION_CN.md
|
||||
2306c6be8fce7841b3244c99dba7deba37777808559679fbee1e2c1340efa18a docs/07_SELF_OPTIMIZATION_AND_ROLLBACK_CN.md
|
||||
bbea36a38447a7122a095601cbb755a17e78964e548e111ba58698cd02d7fe79 docs/08_DOMAIN_ROUTE_PLAN_CN.md
|
||||
56de0cd84602e9a6a31cb35d3cefddd5e228a565653e8774e15f2d0b10969b28 docs/09_BASELINE_PRESERVATION_CN.md
|
||||
ddb6afc02a5dd3eb6c2caeb1a0e303f5871af24da49f337f136c79daba6ddc3c docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md
|
||||
a061bbda9174a118c1b3bbfd9a923822cd59ccd1064d5a1269d9867a025a513b docs/11_USER_REQUIREMENTS_V1R1C0_CN.md
|
||||
48250350a3c3c8fa041f1846d022ee044265aa7502b2d5fd8b65c0a5d0c7d423 docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md
|
||||
1abab047f7eb498c5210fd4036168c8d0f87a360db34018aee6d8ab9e8d894cb docs/13_CODEX_EXECUTION_FULL_V1R1C0_CN.md
|
||||
fceb3e9dd8c89aa2909b063fb3ee257cb2a2580c2c734c12c8479f9d15a17428 docs/14_TEST_PLAN_UAT_V1R1C0_CN.md
|
||||
a74f0024620e0bdcaa83638b2592c9bf36b2b489e264bb9dc166f7ff4e920267 docs/15_SECURITY_AND_PERMISSION_V1R1C0_CN.md
|
||||
e497a51946abbcb83a759ad21b5bfdcbe585140c918f152672dbb7f52cf2185a docs/16_MODEL_ROUTING_CUSTOM_LLM_CN.md
|
||||
84d5c4cf5ccd85eb1e5629788825ffb800c2b733011b1d0265dc444846228ee3 docs/17_NPM_SHORT_LINKS_CN.md
|
||||
5fcb741ef3cad5281ae6524886a9b2425111e7d999773064d85b8184eacd7030 docs/18_RUNTIME_SELECTOR_MULTIUSER_CN.md
|
||||
105a95d5862455878c04c10491f15b013fa059d62b16b9d53e959f64eb7b6a72 docs/19_GITOPS_BASELINE_VXRXC_CN.md
|
||||
43c5c398b386ca75e2ea5e352d74b149731a5a728ed07cab11a65316674ac5a7 docs/20_DEPLOYMENT_RUNBOOK_V1R1C0_CN.md
|
||||
cc5082cfd7379b4fb90531f958178b2513f2486530910046702218a03036e5e7 docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md
|
||||
d8cea35eb6c1300d20804ef480312d26dbdc2ad3a2d26b9430c6ca4808155a95 fixes/patch_runtime_owner_admin_user.py
|
||||
6a79b5958fdda447d3177561959c4f2a3b9c55ab32e16619a3d4ad3965979929 fixes/patch_runtime_v5_guards.py
|
||||
0eb12e0161371464605577707c84df413b555277933f9eb4488ab38032ff699b migrations/001_webops_core_sqlite.sql
|
||||
6d61da88e7d6b208e7eee7195b521dc47cb68587f91987d7a0dd1c8801ef168d migrations/002_seed_owner_admin_user.sql
|
||||
a2c0c7699e1eb46cfdeabfc905cb79b71ed14fb6bcf81ecc99ec47975cf05767 migrations/004_gitops_custom_llm_runtime.sql
|
||||
b77bce1ca78cede27de6594e25f6ce5bb0c4990c0953eeb79f7e2608d2515c32 package_verify_full.sh
|
||||
f65b7fba5d6bdac3070ff4309830e286cd6319a80ab2e55263e46022fed2bf7f scripts/apply_migrations.py
|
||||
bc0452d261def2366cdefd845e62d7177b42806f93ba78294ece29a8a369157e scripts/backup_before_fix.sh
|
||||
bdb439086c86b3b23f8240cb2bef0f757a6be242d21d13a79eb1e44434efbe2e scripts/create_patch.sh
|
||||
13ed8b9f9b2f615f97945da559181ec4a6d6d1897963e5a24c8a3741afa90300 scripts/export_evidence.sh
|
||||
1fdeecf0315f3ab4ec461c3376128054df9ac48e2fe5a3f64a138cd7d710d44d scripts/generate_release_manifest.py
|
||||
80c6c10f259245681c42932f3f5f9b9a0fbd9cadce6a3f495d82cbdbc8618c73 scripts/healthcheck.sh
|
||||
3795cadfa6429a434c955913dbdcbf552181c88301a196b4b458f4f5dd206cd8 scripts/init_gitops_baseline.sh
|
||||
4d54a4ed6d35bbb6e9fe2924f3cfceeea9c3eed898017788f6c5ab8041394a47 scripts/install_v1r1c0_gitops_model_overlay.sh
|
||||
0064539ac1e1bd32dc8062ecc307ad2bc8520895009583a72d46868d1cf44e3c scripts/install_webops_overlay.sh
|
||||
9fadafc4b4d098c006d159cb3b401eb19cf9ab9c5fa4b5cb109e946501c0b8e1 scripts/owner_wechat_notify_worker.py
|
||||
e0abd8ad44d4f088108f2046fd9d7bbd4465af50f79bd3b9fcbb72a8c61bee65 scripts/package_contract_check_v1r1c0.sh
|
||||
abef8df642c6d233af4a03c512876b02b326e19c924020b8dd8cc74236d18817 scripts/release_version.sh
|
||||
bbdb3f9992b203ddc2a6a83241052663de4f021d0705724bd9df05acaa99b022 scripts/rollback_last_delta.sh
|
||||
0814c26ecf6732cf2c24f5f414ad0128a159768cfbbeb21d557274064feb1188 scripts/rollback_version.sh
|
||||
da84a5cc61c1d54a7d1e96e4b0a7186e192edf3ec12d694fc9d7608c3b1086ef scripts/run_acceptance_with_autofix.sh
|
||||
0f55c3350bb15210382a934eaeda94480bb4916bd5edb08b82d78a965e3fabc9 scripts/run_full_uat.py
|
||||
1adcccf655dfad5f31d770b877ebce05003fc76013bb80748cacfb58f7d0cbd0 scripts/run_v1r1c0_uat.py
|
||||
d11050b67a7c497da01d41092ac09204f899f718249cf292b4aa3c898e4e8a3d scripts/run_v5_uat.py
|
||||
0b590dbd8470b13ca79ca26303ced851c39ad2e219888540aab8f818e2935ff3 scripts/run_webops_local.sh
|
||||
d9212d3798ed5f513ff731413882183520299e2d2cd3675bfc5e6d252ebaf00a scripts/run_wechat_behavior_simulation.py
|
||||
ab4666f2b56094c8b982ed5f03815d053b9bd9a1b37886c54ca9e6fa009f4518 scripts/self_diagnose.py
|
||||
65b70921eec67c7890527908e980dc63da6049a72bf47f7ed6496673b14705db scripts/self_diagnose_v5.py
|
||||
ab2421e73619f569e8eecf651fe71bf8435c05e953e8fec2c8e9b5fd1a8e94ba scripts/self_optimize.py
|
||||
6e06c0a0c53d3c3402df38dcfd326532bf4a360ec2651e245209747fdfc72b9c scripts/self_optimize_v5.py
|
||||
5525f0f5d5af788185260ae2c14512e73bb5857862e7445974a4967c49c980d4 scripts/silent_install_v5.sh
|
||||
1bc3bebd37f2112df27301d23527e150b79efbdaf0b2ed04dbb8f970f9d53686 scripts/smoke_test.sh
|
||||
2fe86ebbbc88124e9e9b9d39cb441a3a450a015162573e87cf70a8693765b0ff scripts/validate_no_legacy_roles.py
|
||||
4a4444b0a60221b6c128eef4514466293dba2d0863eee4aee6f9d9b16b727045 scripts/validate_roles_and_domains.py
|
||||
a9bb2c30bf45c7ab897b439ec02286d2a4e8d946ca362da30bace445ad2af58a source-baseline/SOURCE_BASELINE_SUMMARY.json
|
||||
37fd43063010b38ebac585cf18f42ea4fb487018729fdb6467cf9a3703cf4ef3 source-baseline/V5_SOURCE_BASELINE_SUMMARY.json
|
||||
8456cf86202ef46f9ca6e61b14f1763f298755e5eb36d42051306620c49f471c source-baseline/current_actual_deployment_source.zip
|
||||
8456cf86202ef46f9ca6e61b14f1763f298755e5eb36d42051306620c49f471c source-baseline/previous_current_actual_deployment_source_from_webops_delta.zip
|
||||
48a9f80446dff49039b38508a70d0785f9e10ec8feb2700df925accf70685318 tests/run_acceptance_with_autofix.sh
|
||||
4948d5f60be329d56bc246d32fb63fad57f0d1b944a7f5412cd935fbf8e7ae54 tests/run_v1r1c0_acceptance.sh
|
||||
3c4675e5e202f1fc4377b662776b543fc312bd5a07cb4e8f2001f5a9f6eeceda tests/run_v5_acceptance_with_autofix.sh
|
||||
f81179a2c940a568a5e5d68f1458f065e92c5636849c37d619e469b696462954 tests/test_custom_llm_registry.py
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"package": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706",
|
||||
"version": "V1R1C0",
|
||||
"base_uploaded_package": "AIOS_v23_v5_digital_life_WEBOPS_PROD_FULL_DEPLOY_20260706(1).zip",
|
||||
"generated_at": "2026-07-06T06:30:48Z",
|
||||
"change_level": "R",
|
||||
"features": [
|
||||
"MultiUser shared/dedicated runtime selector",
|
||||
"GitOps online baseline and VxRxCx versioning",
|
||||
"User custom LLM registry",
|
||||
"User-defined model priority and fallback",
|
||||
"NPM short-link integration",
|
||||
"OpenClaw and Hermes adapters",
|
||||
"Full API contract and acceptance tests"
|
||||
],
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"short_links": [
|
||||
"/git",
|
||||
"/base",
|
||||
"/ver",
|
||||
"/diff",
|
||||
"/patch",
|
||||
"/model",
|
||||
"/run",
|
||||
"/api",
|
||||
"/hook"
|
||||
],
|
||||
"main_docs": [
|
||||
"docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md",
|
||||
"docs/11_USER_REQUIREMENTS_V1R1C0_CN.md",
|
||||
"docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md",
|
||||
"docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# AIOS V1R1C0 MultiUser GitOps Custom LLM 生产工程部署包
|
||||
|
||||
本包基于 `AIOS_v23_v5_digital_life_WEBOPS_PROD_FULL_DEPLOY_20260706` 刷新,版本号升级为:`V1R1C0`。
|
||||
|
||||
## 本版新增能力
|
||||
|
||||
1. 借鉴 MultiUserClaw 的 Platform Gateway / shared + dedicated runtime 思路,但复用现有 AIOS WebOps、NPM、Casdoor、OpenClaw、Hermes。
|
||||
2. 建立 GitOps 在线基线机制,后续不再定期上传 ZIP,通过 Git branch / patch / tag / diff 刷新。
|
||||
3. 支持 `VxRxCx` 版本体系:V=架构,R=功能,C=修复。
|
||||
4. 支持用户在基础模型之外添加自定义 LLM 模型,并自主设置模型优先级与 fallback 顺序。
|
||||
5. 新增 GitOps + Model Registry + Runtime Selector API 适配器,默认监听 `127.0.0.1:18890`。
|
||||
6. 保留短链接:`/git /base /ver /diff /patch /run /api /model /hook`。
|
||||
7. 通过 NPM 暴露入口,避免新增裸 Nginx 配置。
|
||||
|
||||
## 推荐执行
|
||||
|
||||
```bash
|
||||
cd <解压目录>
|
||||
bash scripts/install_v1r1c0_gitops_model_overlay.sh
|
||||
bash tests/run_v1r1c0_acceptance.sh
|
||||
```
|
||||
|
||||
## 给 Codex 的完整执行文档
|
||||
|
||||
```text
|
||||
docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md
|
||||
```
|
||||
|
||||
本包是 V1R1C0 全量工程部署包,包含文档、脚本、迁移、接口适配器、测试用例。部署时应由 Codex 在服务器上定位当前生产包和运行目录,按增量方式叠加,不得破坏现有 WebOps/NPM/Casdoor/OpenClaw/Hermes。
|
||||
Executable
+244
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import json, os, re, sqlite3, subprocess, time, uuid, hashlib, urllib.parse, html
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
|
||||
WORKSPACE = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
|
||||
DB_PATH = Path(os.getenv('AIOS_GITOPS_DB', str(ROOT / 'runtime' / 'gitops_model.sqlite3')))
|
||||
SECRETS_DIR = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
|
||||
TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS_DIR / 'gitops.tokens')))
|
||||
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
|
||||
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
|
||||
ENTRY = os.getenv('AIOS_ENTRY', 'https://cnnetmkt.top')
|
||||
DEV_ALLOW = os.getenv('AIOS_GITOPS_DEV_ALLOW', '0') == '1'
|
||||
BASE_MODELS = [
|
||||
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display':'DeepSeek V4 Flash','tier':'regular','reasoning':1,'vision':0,'tools':1,'context':128000},
|
||||
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display':'Qwen 3.6 35B','tier':'pro','reasoning':1,'vision':0,'tools':1,'context':262144},
|
||||
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display':'GPT-5.5','tier':'ultra','reasoning':1,'vision':1,'tools':1,'context':256000},
|
||||
]
|
||||
|
||||
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
def uid(prefix='id') -> str: return f'{prefix}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
|
||||
def esc(x: Any) -> str: return html.escape('' if x is None else str(x))
|
||||
def safe_name(s: str) -> str: return re.sub(r'[^a-zA-Z0-9_.@-]+', '_', s or 'unknown')[:120]
|
||||
def db():
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True); con = sqlite3.connect(DB_PATH); con.row_factory = sqlite3.Row; return con
|
||||
def rows(sql, args=()):
|
||||
with db() as con: return [dict(r) for r in con.execute(sql, args).fetchall()]
|
||||
def one(sql, args=()):
|
||||
r = rows(sql, args); return r[0] if r else None
|
||||
def exec_sql(sql, args=()):
|
||||
with db() as con: con.execute(sql, args); con.commit()
|
||||
def audit(actor, action, target='', payload=None): exec_sql('INSERT INTO model_audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'), actor, action, target, json.dumps(payload or {}, ensure_ascii=False), now()))
|
||||
|
||||
def init_db():
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
schema = Path(__file__).resolve().parents[1] / 'migrations' / '004_gitops_custom_llm_runtime.sql'
|
||||
with db() as con:
|
||||
if schema.exists(): con.executescript(schema.read_text(encoding='utf-8'))
|
||||
else:
|
||||
con.executescript('''CREATE TABLE IF NOT EXISTS gitops_baseline(id TEXT PRIMARY KEY,active_version TEXT,active_tag TEXT,source_package TEXT,entry TEXT,update_mode TEXT,status TEXT,created_at TEXT,updated_at TEXT);CREATE TABLE IF NOT EXISTS gitops_versions(version TEXT PRIMARY KEY,tag TEXT,level TEXT,summary TEXT,status TEXT,created_by TEXT,created_at TEXT);CREATE TABLE IF NOT EXISTS gitops_patches(id TEXT PRIMARY KEY,patch_name TEXT,branch TEXT,base_ref TEXT,target_version TEXT,risk TEXT,status TEXT,created_by TEXT,created_at TEXT,applied_at TEXT,result TEXT);CREATE TABLE IF NOT EXISTS llm_providers(id TEXT PRIMARY KEY,owner_username TEXT,name TEXT,provider_type TEXT,base_url TEXT,auth_mode TEXT,api_key_secret_ref TEXT,scope TEXT,status TEXT,created_at TEXT,updated_at TEXT);CREATE TABLE IF NOT EXISTS llm_models(id TEXT PRIMARY KEY,provider_id TEXT,owner_username TEXT,model_name TEXT,display_name TEXT,context_window INTEGER,input_modalities TEXT,output_modalities TEXT,supports_tools INTEGER,supports_vision INTEGER,supports_reasoning INTEGER,cost_tier TEXT,is_base INTEGER,scope TEXT,status TEXT,created_at TEXT,updated_at TEXT, UNIQUE(owner_username, provider_id, model_name));CREATE TABLE IF NOT EXISTS user_model_priority(username TEXT,model_id TEXT,priority INTEGER,enabled INTEGER,fallback_policy TEXT,updated_at TEXT,PRIMARY KEY(username,model_id));CREATE TABLE IF NOT EXISTS model_route_decisions(id TEXT PRIMARY KEY,username TEXT,task_type TEXT,selected_model_id TEXT,candidate_order TEXT,reason TEXT,created_at TEXT);CREATE TABLE IF NOT EXISTS user_runtime_profiles(username TEXT PRIMARY KEY,role TEXT,tier TEXT,runtime_mode TEXT,allow_dedicated INTEGER,allow_custom_llm INTEGER,updated_at TEXT);CREATE TABLE IF NOT EXISTS runtime_instances(id TEXT PRIMARY KEY,username TEXT,runtime_type TEXT,backend TEXT,status TEXT,endpoint TEXT,last_seen_at TEXT,created_at TEXT,metadata TEXT);CREATE TABLE IF NOT EXISTS model_audit_logs(id TEXT PRIMARY KEY,actor TEXT,action TEXT,target TEXT,payload TEXT,created_at TEXT);''')
|
||||
con.commit()
|
||||
seed_base_models()
|
||||
|
||||
def seed_base_models():
|
||||
exec_sql("INSERT OR IGNORE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ('provider.one-api','system','AIOS One API','openai_compatible','http://127.0.0.1:13000/v1','bearer','secret://one-api/global','global','active',now(),now()))
|
||||
exec_sql("INSERT OR IGNORE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ('provider.openai','system','OpenAI Official','openai_compatible','https://api.openai.com/v1','bearer','secret://openai/global','global','active',now(),now()))
|
||||
for m in BASE_MODELS:
|
||||
pid = 'provider.openai' if m['provider']=='openai' else 'provider.one-api'
|
||||
exec_sql('''INSERT OR IGNORE INTO llm_models(id,provider_id,owner_username,model_name,display_name,context_window,input_modalities,output_modalities,supports_tools,supports_vision,supports_reasoning,cost_tier,is_base,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (m['id'], pid, 'system', m['model'], m['display'], m['context'], '["text"]', '["text"]', m['tools'], m['vision'], m['reasoning'], m['tier'], 1, 'global', 'active', now(), now()))
|
||||
if not one('SELECT * FROM gitops_baseline WHERE id=?', ('current',)):
|
||||
exec_sql('INSERT INTO gitops_baseline(id,active_version,active_tag,source_package,entry,update_mode,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)', ('current','V1R1C0','V1R1C0','AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706',ENTRY,'git_patch_only','active',now(),now()))
|
||||
if not one('SELECT * FROM gitops_versions WHERE version=?', ('V1R1C0',)):
|
||||
exec_sql('INSERT INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)', ('V1R1C0','V1R1C0','R','MultiUser runtime selector + GitOps + custom LLM priority registry','ready','system',now()))
|
||||
|
||||
def valid_token(token: str) -> bool:
|
||||
if DEV_ALLOW: return True
|
||||
if not token or not TOKEN_FILE.exists(): return False
|
||||
return token.strip() in [x.strip() for x in TOKEN_FILE.read_text().splitlines() if x.strip()]
|
||||
def git_cmd(args, check=False):
|
||||
WORKSPACE.mkdir(parents=True, exist_ok=True); p = subprocess.run(['git'] + args, cwd=str(WORKSPACE), text=True, capture_output=True)
|
||||
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git command failed')
|
||||
return {'code': p.returncode, 'stdout': p.stdout.strip(), 'stderr': p.stderr.strip(), 'cmd': ['git']+args}
|
||||
def model_visible_to(username: str): return rows('''SELECT m.*, p.name provider_name, p.base_url, p.provider_type, p.scope provider_scope FROM llm_models m JOIN llm_providers p ON p.id=m.provider_id WHERE m.status='active' AND (m.scope='global' OR m.owner_username=? OR p.owner_username=?) ORDER BY m.is_base DESC, m.created_at ASC''', (username, username))
|
||||
def priority_for(username: str): return rows('''SELECT pr.username, pr.model_id, pr.priority, pr.enabled, pr.fallback_policy, pr.updated_at, m.model_name, m.display_name, m.is_base, m.owner_username, p.base_url, p.name provider_name FROM user_model_priority pr JOIN llm_models m ON m.id=pr.model_id JOIN llm_providers p ON p.id=m.provider_id WHERE pr.username=? AND m.status='active' ORDER BY pr.priority ASC''', (username,))
|
||||
def route_model(username: str, task_type='chat'):
|
||||
ordered = [r for r in priority_for(username) if int(r.get('enabled') or 0) == 1]
|
||||
if not ordered:
|
||||
ordered = [m for m in model_visible_to(username) if int(m.get('is_base') or 0) == 1]
|
||||
selected = dict(ordered[0]) if ordered else None
|
||||
if selected and 'id' not in selected and selected.get('model_id'):
|
||||
selected['id'] = selected.get('model_id')
|
||||
reason = 'user_priority' if priority_for(username) else 'role_default_base_model'
|
||||
candidate_ids = [(x.get('id') or x.get('model_id')) for x in ordered]
|
||||
selected_id = selected.get('id') if selected else None
|
||||
exec_sql('INSERT INTO model_route_decisions(id,username,task_type,selected_model_id,candidate_order,reason,created_at) VALUES(?,?,?,?,?,?,?)',(uid('route'), username, task_type, selected_id, json.dumps(candidate_ids, ensure_ascii=False), reason, now()))
|
||||
return {'username': username, 'task_type': task_type, 'selected': selected, 'candidate_order': ordered, 'reason': reason}
|
||||
def parse_body(handler):
|
||||
n = int(handler.headers.get('Content-Length','0') or '0')
|
||||
if n <= 0: return {}
|
||||
raw = handler.rfile.read(n).decode('utf-8')
|
||||
try: return json.loads(raw or '{}')
|
||||
except Exception: return {'_raw': raw}
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = 'AIOSGitOpsModelAPI/V1R1C0'
|
||||
def actor(self):
|
||||
auth = self.headers.get('Authorization',''); token = auth.replace('Bearer ', '').strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
|
||||
ok = valid_token(token); user = self.headers.get('X-AIOS-User') or ('dev-owner' if DEV_ALLOW else 'anonymous'); role = self.headers.get('X-AIOS-Role') or ('owner' if DEV_ALLOW else 'user')
|
||||
return {'username': safe_name(user), 'role': role, 'token_ok': ok}
|
||||
def require_write(self, roles=None):
|
||||
a = self.actor()
|
||||
if not a['token_ok']: self.send_json({'error':'unauthorized','hint':'write APIs require Bearer token or AIOS_GITOPS_DEV_ALLOW=1'}, 401); return None
|
||||
if roles and a['role'] not in roles: self.send_json({'error':'forbidden','required_roles':roles,'role':a['role']}, 403); return None
|
||||
return a
|
||||
def send_json(self, data, code=200):
|
||||
b = json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'); self.send_response(code); self.send_header('Content-Type','application/json; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
|
||||
def send_html(self, title, body, code=200):
|
||||
links = '<a href="/base">base</a> <a href="/ver">ver</a> <a href="/diff">diff</a> <a href="/patch">patch</a> <a href="/run">run</a> <a href="/model">model</a> <a href="/api">api</a>'
|
||||
html_doc = f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:24px;background:#f8fafc}}a{{margin-right:12px}}.card{{background:white;border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin:12px 0}}table{{border-collapse:collapse;width:100%;background:white}}td,th{{border:1px solid #ddd;padding:6px;text-align:left;vertical-align:top}}pre{{background:#0f172a;color:#e2e8f0;padding:12px;border-radius:8px;overflow:auto}}</style></head><body><h1>{esc(title)}</h1><div class="card">{links}</div>{body}</body></html>'''
|
||||
b = html_doc.encode('utf-8'); self.send_response(code); self.send_header('Content-Type','text/html; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
|
||||
def table(self, data):
|
||||
if not data: return '<p>No records.</p>'
|
||||
cols = list(data[0].keys()); return '<table><thead><tr>'+''.join(f'<th>{esc(c)}</th>' for c in cols)+'</tr></thead><tbody>'+''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)+'</tbody></table>'
|
||||
def do_GET(self):
|
||||
init_db(); u = urllib.parse.urlparse(self.path); path = u.path; q = urllib.parse.parse_qs(u.query); username = self.actor()['username']
|
||||
try:
|
||||
if path in ('/health','/api/health'): self.send_json({'status':'ok','version':'V1R1C0','entry':ENTRY,'workspace':str(WORKSPACE),'db':str(DB_PATH)}); return
|
||||
if path in ('/api/status','/api'):
|
||||
if path == '/api': self.send_html('AIOS V1R1C0 API', '<pre>'+esc(json.dumps(api_spec(),ensure_ascii=False,indent=2))+'</pre>'); return
|
||||
self.send_json({'version':'V1R1C0','git':git_cmd(['status','--short']),'baseline':one('SELECT * FROM gitops_baseline WHERE id=?',('current',))}); return
|
||||
if path in ('/api/base','/base'):
|
||||
data = one('SELECT * FROM gitops_baseline WHERE id=?',('current',)) or {}; self.send_html('Baseline', '<pre>'+esc(json.dumps(data,ensure_ascii=False,indent=2))+'</pre>') if path == '/base' else self.send_json(data); return
|
||||
if path in ('/api/ver','/ver'):
|
||||
data = rows('SELECT * FROM gitops_versions ORDER BY created_at DESC'); self.send_html('Versions', self.table(data)) if path == '/ver' else self.send_json(data); return
|
||||
if path in ('/api/patch','/patch'):
|
||||
data = rows('SELECT * FROM gitops_patches ORDER BY created_at DESC'); self.send_html('Patches', self.table(data)) if path == '/patch' else self.send_json(data); return
|
||||
if path in ('/api/diff','/diff'):
|
||||
frm = (q.get('from') or q.get('from_') or ['HEAD~1'])[0]; to = (q.get('to') or ['HEAD'])[0]; diff = git_cmd(['diff', frm, to])
|
||||
self.send_html('Diff', f'<p>from={esc(frm)} to={esc(to)}</p><pre>{esc(diff["stdout"] or diff["stderr"])}</pre>') if path == '/diff' else self.send_json({'from':frm,'to':to,'diff':diff}); return
|
||||
if path in ('/api/model/base','/api/model/custom','/api/model/priority','/model'):
|
||||
if path == '/api/model/base': self.send_json([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==1]); return
|
||||
if path == '/api/model/custom': self.send_json([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==0]); return
|
||||
if path == '/api/model/priority': self.send_json(priority_for(username)); return
|
||||
body = '<h2>Base Models</h2>'+self.table([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==1]); body += '<h2>Custom Models</h2>'+self.table([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==0]); body += '<h2>Priority</h2>'+self.table(priority_for(username)); self.send_html('Model Registry', body); return
|
||||
if path in ('/api/runtime/status','/run'):
|
||||
data = rows('SELECT * FROM runtime_instances ORDER BY created_at DESC LIMIT 100'); self.send_html('Runtime', self.table(data)) if path == '/run' else self.send_json(data); return
|
||||
if path == '/api/audit': self.send_json(rows('SELECT * FROM model_audit_logs ORDER BY created_at DESC LIMIT 200')); return
|
||||
self.send_json({'error':'not_found','path':path},404)
|
||||
except Exception as e: self.send_json({'error':str(e)},500)
|
||||
def do_POST(self):
|
||||
init_db(); path = urllib.parse.urlparse(self.path).path; data = parse_body(self)
|
||||
try:
|
||||
if path == '/api/model/custom/add':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
username = safe_name(data.get('username') or a['username']); provider_name = data.get('provider_name') or data.get('name') or 'custom-provider'; base_url = (data.get('base_url') or '').rstrip('/'); model_name = data.get('model_name') or data.get('model')
|
||||
if not base_url or not model_name: self.send_json({'error':'base_url and model_name required'},400); return
|
||||
secret_ref = data.get('api_key_secret_ref')
|
||||
if data.get('api_key'):
|
||||
sdir = SECRETS_DIR / 'llm' / safe_name(username); sdir.mkdir(parents=True, exist_ok=True); secret_id = uid('llmkey'); sp = sdir / f'{secret_id}.secret'; sp.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(sp, 0o600); secret_ref = f'secret://llm/{safe_name(username)}/{secret_id}'
|
||||
provider_id = data.get('provider_id') or f'provider.custom.{safe_name(username)}.{hashlib.sha1((provider_name+base_url).encode()).hexdigest()[:8]}'; model_id = data.get('model_id') or f'custom.{safe_name(username)}.{hashlib.sha1((provider_id+model_name).encode()).hexdigest()[:10]}'
|
||||
exec_sql('''INSERT OR REPLACE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)''',(provider_id, username, provider_name, data.get('provider_type','openai_compatible'), base_url, data.get('auth_mode','bearer'), secret_ref, data.get('scope','user'), 'active', now(), now()))
|
||||
exec_sql('''INSERT OR REPLACE INTO llm_models(id,provider_id,owner_username,model_name,display_name,context_window,input_modalities,output_modalities,supports_tools,supports_vision,supports_reasoning,cost_tier,is_base,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',(model_id, provider_id, username, model_name, data.get('display_name') or model_name, int(data.get('context_window') or 0), json.dumps(data.get('input_modalities') or ['text'],ensure_ascii=False), json.dumps(data.get('output_modalities') or ['text'],ensure_ascii=False), int(bool(data.get('supports_tools',1))), int(bool(data.get('supports_vision',0))), int(bool(data.get('supports_reasoning',0))), data.get('cost_tier','custom'), 0, data.get('scope','user'), 'active', now(), now()))
|
||||
if 'priority' in data: exec_sql('INSERT OR REPLACE INTO user_model_priority(username,model_id,priority,enabled,fallback_policy,updated_at) VALUES(?,?,?,?,?,?)',(username, model_id, int(data.get('priority')), int(bool(data.get('enabled',1))), data.get('fallback_policy','try_next_on_error'), now()))
|
||||
audit(a['username'],'custom_model_add',model_id,{'username':username,'provider_id':provider_id,'model_name':model_name}); self.send_json({'status':'ok','provider_id':provider_id,'model_id':model_id,'secret_ref':secret_ref}); return
|
||||
if path == '/api/model/custom/update':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
model_id = data.get('model_id'); m = one('SELECT * FROM llm_models WHERE id=?',(model_id,))
|
||||
if not m: self.send_json({'error':'model not found'},404); return
|
||||
if m['owner_username'] != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
|
||||
fields=[]; args=[]
|
||||
for col in ['display_name','status','cost_tier']:
|
||||
if col in data: fields.append(f'{col}=?'); args.append(data[col])
|
||||
for col in ['context_window','supports_tools','supports_vision','supports_reasoning']:
|
||||
if col in data: fields.append(f'{col}=?'); args.append(int(bool(data[col])) if col.startswith('supports') else int(data[col]))
|
||||
fields.append('updated_at=?'); args.append(now()); args.append(model_id); exec_sql(f'UPDATE llm_models SET {", ".join(fields)} WHERE id=?', args); audit(a['username'],'custom_model_update',model_id,data); self.send_json({'status':'updated','model_id':model_id}); return
|
||||
if path == '/api/model/delete':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
model_id = data.get('model_id'); m = one('SELECT * FROM llm_models WHERE id=?',(model_id,))
|
||||
if not m: self.send_json({'error':'model not found'},404); return
|
||||
if int(m.get('is_base') or 0)==1: self.send_json({'error':'base model cannot be deleted'},400); return
|
||||
if m['owner_username'] != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
|
||||
exec_sql('UPDATE llm_models SET status=?,updated_at=? WHERE id=?',('disabled',now(),model_id)); audit(a['username'],'custom_model_delete',model_id,{}); self.send_json({'status':'disabled','model_id':model_id}); return
|
||||
if path == '/api/model/priority/set':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
username = safe_name(data.get('username') or a['username'])
|
||||
if username != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
|
||||
items = data.get('items') or data.get('priority') or []; exec_sql('DELETE FROM user_model_priority WHERE username=?',(username,)); visible={m['id'] for m in model_visible_to(username)}
|
||||
for idx,item in enumerate(items):
|
||||
if isinstance(item,str): item={'model_id':item,'priority':(idx+1)*10,'enabled':1}
|
||||
mid=item.get('model_id') or item.get('id')
|
||||
if mid and mid in visible: exec_sql('INSERT OR REPLACE INTO user_model_priority(username,model_id,priority,enabled,fallback_policy,updated_at) VALUES(?,?,?,?,?,?)',(username, mid, int(item.get('priority',(idx+1)*10)), int(bool(item.get('enabled',1))), item.get('fallback_policy','try_next_on_error'), now()))
|
||||
audit(a['username'],'model_priority_set',username,{'items':items}); self.send_json({'status':'updated','username':username,'priority':priority_for(username)}); return
|
||||
if path == '/api/model/route/resolve': a=self.actor(); self.send_json(route_model(safe_name(data.get('username') or a['username']), data.get('task_type','chat'))); return
|
||||
if path == '/api/model/test':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
mid=data.get('model_id'); m=one('SELECT m.*,p.base_url,p.provider_type FROM llm_models m JOIN llm_providers p ON p.id=m.provider_id WHERE m.id=?',(mid,))
|
||||
if not m: self.send_json({'error':'model not found'},404); return
|
||||
self.send_json({'status':'ok' if m['base_url'] and m['model_name'] else 'failed','live_test':os.getenv('AIOS_ENABLE_MODEL_LIVE_TEST','0')=='1','model':m['model_name'],'base_url':m['base_url'],'note':'Network live call is disabled unless AIOS_ENABLE_MODEL_LIVE_TEST=1'}); return
|
||||
if path == '/api/runtime/select':
|
||||
a=self.actor(); username=safe_name(data.get('username') or a['username']); role=data.get('role') or a['role']; tier=data.get('tier') or ('ultra' if role=='owner' else 'regular'); risk=data.get('risk','normal')
|
||||
if risk in ('high_tool','shell','docker','write_prod'): runtime,backend='ephemeral_sandbox','openclaw_executor'
|
||||
elif role in ('owner','admin') or tier=='ultra': runtime,backend='dedicated','hermes'
|
||||
elif tier=='pro': runtime,backend='dedicated_or_shared','hermes'
|
||||
else: runtime,backend='shared','hermes'
|
||||
exec_sql('INSERT OR REPLACE INTO user_runtime_profiles(username,role,tier,runtime_mode,allow_dedicated,allow_custom_llm,updated_at) VALUES(?,?,?,?,?,?,?)',(username,role,tier,runtime,1 if runtime!='shared' else 0,1,now())); self.send_json({'username':username,'role':role,'tier':tier,'runtime_type':runtime,'backend':backend,'reason':'V1R1C0 selector policy'}); return
|
||||
if path == '/api/runtime/register':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
rid=data.get('id') or uid('rt'); exec_sql('INSERT OR REPLACE INTO runtime_instances(id,username,runtime_type,backend,status,endpoint,last_seen_at,created_at,metadata) VALUES(?,?,?,?,?,?,?,?,?)',(rid,safe_name(data.get('username') or 'shared'),data.get('runtime_type','shared'),data.get('backend','hermes'),data.get('status','registered'),data.get('endpoint'),now(),now(),json.dumps(data.get('metadata') or {},ensure_ascii=False))); audit(a['username'],'runtime_register',rid,data); self.send_json({'status':'registered','id':rid}); return
|
||||
if path == '/api/runtime/heartbeat': exec_sql('UPDATE runtime_instances SET status=?,last_seen_at=?,metadata=? WHERE id=?',(data.get('status','ok'),now(),json.dumps(data.get('metadata') or {},ensure_ascii=False),data.get('id'))); self.send_json({'status':'ok','id':data.get('id')}); return
|
||||
if path == '/api/branch':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
branch=data.get('branch'); base=data.get('base','main')
|
||||
if not branch: self.send_json({'error':'branch required'},400); return
|
||||
git_cmd(['fetch','--all']); git_cmd(['checkout',base]); git_cmd(['checkout','-B',branch],check=True); audit(a['username'],'git_branch',branch,data); self.send_json({'status':'ok','branch':branch}); return
|
||||
if path == '/api/file':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
branch=data.get('branch') or 'main'; rel=data.get('path'); content=data.get('content',''); msg=data.get('message') or f'chore(gitops): update {rel}'
|
||||
if not rel or rel.startswith('/') or '..' in Path(rel).parts: self.send_json({'error':'invalid path'},400); return
|
||||
git_cmd(['checkout',branch],check=True); target=WORKSPACE/rel; target.parent.mkdir(parents=True,exist_ok=True); target.write_text(content,encoding='utf-8'); git_cmd(['add',rel],check=True); commit=git_cmd(['commit','-m',msg]); audit(a['username'],'git_file_update',rel,{'branch':branch,'commit':commit}); self.send_json({'status':'committed','path':rel,'commit':commit}); return
|
||||
if path == '/api/commit':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
msg=data.get('message') or 'chore(gitops): commit changes'; git_cmd(['add','.'],check=True); out=git_cmd(['commit','-m',msg]); audit(a['username'],'git_commit','workspace',{'message':msg}); self.send_json({'status':'ok','commit':out}); return
|
||||
if path == '/api/tag':
|
||||
a = self.require_write(['owner']);
|
||||
if not a: return
|
||||
ver=data.get('version') or data.get('tag')
|
||||
if not re.match(r'^V\d+R\d+C\d+(-[a-zA-Z0-9_.-]+)?$', ver or ''): self.send_json({'error':'version must match VxRxCx'},400); return
|
||||
msg=data.get('message') or f'release {ver}'; out=git_cmd(['tag','-a',ver,'-m',msg]);
|
||||
if data.get('push',False): git_cmd(['push','origin',ver])
|
||||
level='V' if re.match(r'^V[1-9]\d*R0C0', ver) else ('R' if re.match(r'^V\d+R[1-9]\d*C0', ver) else 'C'); exec_sql('INSERT OR REPLACE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(ver,ver,level,msg,'ready',a['username'],now())); exec_sql('UPDATE gitops_baseline SET active_version=?,active_tag=?,updated_at=? WHERE id=?',(ver,ver,now(),'current')); audit(a['username'],'git_tag',ver,{'level':level}); self.send_json({'status':'tagged','version':ver,'git':out}); return
|
||||
if path == '/api/rollback':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
target=data.get('target_version') or data.get('version')
|
||||
if not target: self.send_json({'error':'target_version required'},400); return
|
||||
out=git_cmd(['checkout',target]); audit(a['username'],'git_rollback',target,{}); self.send_json({'status':'rollback_checkout','target':target,'git':out}); return
|
||||
if path == '/api/nl':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
text=data.get('text',''); intent='unknown'; plan=[]
|
||||
if '回滚' in text or 'rollback' in text.lower(): intent='rollback'; plan=['extract target VxRxCx','call /api/rollback','run smoke test']
|
||||
elif '对比' in text or 'diff' in text.lower(): intent='diff'; plan=['extract from/to refs','call /api/diff']
|
||||
elif '模型' in text and ('优先' in text or 'priority' in text.lower()): intent='set_model_priority'; plan=['resolve model ids','call /api/model/priority/set']
|
||||
elif '模型' in text and ('添加' in text or '新增' in text or 'add' in text.lower()): intent='add_custom_model'; plan=['validate provider','store api key as secret_ref','call /api/model/custom/add']
|
||||
elif 'patch' in text.lower() or '补丁' in text: intent='create_patch'; plan=['create patch branch','commit patch metadata','run tests']
|
||||
self.send_json({'intent':intent,'action_plan':plan,'execute':False,'safety_check':'V-level changes require owner approval'}); return
|
||||
self.send_json({'error':'not_found','path':path},404)
|
||||
except Exception as e: self.send_json({'error':str(e)},500)
|
||||
|
||||
def api_spec(): return {'version':'V1R1C0','short_links':['/base','/ver','/diff','/patch','/run','/model','/api'],'gitops_get':['/api/status','/api/base','/api/ver','/api/diff','/api/patch'],'gitops_post':['/api/branch','/api/file','/api/commit','/api/tag','/api/rollback','/api/nl'],'model_get':['/api/model/base','/api/model/custom','/api/model/priority'],'model_post':['/api/model/custom/add','/api/model/custom/update','/api/model/delete','/api/model/priority/set','/api/model/route/resolve','/api/model/test'],'runtime':['/api/runtime/status','/api/runtime/select','/api/runtime/register','/api/runtime/heartbeat'],'auth':'write APIs require Bearer token unless AIOS_GITOPS_DEV_ALLOW=1 for local acceptance tests'}
|
||||
def main(): init_db(); print(f'AIOS V1R1C0 GitOps Model API listening on {HOST}:{PORT}'); ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
|
||||
if __name__ == '__main__': main()
|
||||
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import hashlib, html, json, os, sqlite3, time, uuid, urllib.parse
|
||||
from http import cookies
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]))
|
||||
DB_PATH = Path(os.getenv('AIOS_WEBOPS_DB', ROOT / 'data' / 'webops.sqlite3'))
|
||||
HOST = os.getenv('AIOS_WEBOPS_HOST','0.0.0.0')
|
||||
PORT = int(os.getenv('AIOS_WEBOPS_PORT','18081'))
|
||||
PUBLIC_DOMAIN = os.getenv('AIOS_PUBLIC_DOMAIN','https://cnnetmkt.top')
|
||||
WEBOPS_DOMAIN = os.getenv('AIOS_WEBOPS_DOMAIN','https://ops.cnnetmkt.top')
|
||||
API_DOMAIN = os.getenv('AIOS_API_DOMAIN','https://api.cnnetmkt.top')
|
||||
WECHAT_DOMAIN = os.getenv('AIOS_WECHAT_CALLBACK_DOMAIN','https://wechat.cnnetmkt.top')
|
||||
SECRET = os.getenv('AIOS_WEBOPS_SECRET','change-me-before-production')
|
||||
ROLES = ['owner','admin','user']
|
||||
NODES = ['server-hw','server-local']
|
||||
PAGES = [('/dashboard','Dashboard'),('/nodes','Nodes'),('/roles','Roles'),('/routing','Routing'),('/failover','Failover'),('/approvals','Approvals'),('/operations','Operations'),('/users','Users'),('/quota','Quota'),('/models','Models'),('/memory','Memory'),('/channels/wechat','WeChat'),('/life','Life Core'),('/patches','Patch Studio'),('/optimization','Optimization'),('/simulation','Simulation'),('/logs','Logs'),('/config','Config'),('/uat','UAT'),('/versions','Versions'),('/evidence','Evidence')]
|
||||
|
||||
def now(): return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
def uid(p='id'): return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
|
||||
def esc(x): return html.escape('' if x is None else str(x))
|
||||
def jdump(x): return json.dumps(x, ensure_ascii=False)
|
||||
def jload(x):
|
||||
try: return json.loads(x or '{}')
|
||||
except Exception: return {}
|
||||
def db():
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
con=sqlite3.connect(DB_PATH); con.row_factory=sqlite3.Row; return con
|
||||
def exec_sql(sql,args=()):
|
||||
with db() as con: con.execute(sql,args); con.commit()
|
||||
def rows(sql,args=()):
|
||||
with db() as con: return [dict(r) for r in con.execute(sql,args).fetchall()]
|
||||
def one(sql,args=()):
|
||||
r=rows(sql,args); return r[0] if r else None
|
||||
def init_db():
|
||||
(ROOT/'data').mkdir(parents=True,exist_ok=True)
|
||||
for f in sorted((ROOT/'migrations').glob('*.sql')):
|
||||
with db() as con:
|
||||
con.executescript(f.read_text(encoding='utf-8')); con.commit()
|
||||
def audit(actor, action, target='', payload=None):
|
||||
exec_sql('INSERT INTO audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'),actor,action,target,jdump(payload or {}),now()))
|
||||
def notify(event_type,payload):
|
||||
exec_sql('INSERT INTO owner_notifications(id,event_type,payload,status,created_at) VALUES(?,?,?,?,?)',(uid('notify'),event_type,jdump(payload),'queued',now()))
|
||||
def hash_pw(p): return hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
|
||||
def check_pw(stored,p): return stored in (p, hash_pw(p))
|
||||
def token(username,role):
|
||||
data=f'{username}|{role}|{int(time.time())}'; sig=hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]; return data+'|'+sig
|
||||
def parse_session(header):
|
||||
if not header: return None
|
||||
c=cookies.SimpleCookie(); c.load(header)
|
||||
if 'aios_session' not in c: return None
|
||||
parts=c['aios_session'].value.split('|')
|
||||
if len(parts)!=4: return None
|
||||
data='|'.join(parts[:3]); sig=parts[3]
|
||||
if hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]!=sig: return None
|
||||
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(parts[0],parts[1],'approved'))
|
||||
def require(user, roles): return bool(user and user['role'] in roles)
|
||||
def table(data):
|
||||
if not data: return '<p class="muted">No records.</p>'
|
||||
cols=list(data[0].keys()); head=''.join(f'<th>{esc(c)}</th>' for c in cols)
|
||||
body=''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)
|
||||
return f'<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>'
|
||||
def nav(user):
|
||||
links=''.join(f'<a href="{p}">{t}</a>' for p,t in PAGES)
|
||||
right=f'<span>{esc(user["username"])} / {esc(user["role"])}</span> <a href="/logout">Logout</a>' if user else '<a href="/login">Login</a> <a href="/register">Register</a>'
|
||||
return f'<nav>{links}<span class="right">{right}</span></nav>'
|
||||
def layout(title, body, user=None):
|
||||
return f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>
|
||||
body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:0;background:#f7f8fb;color:#111}}nav{{background:#111827;color:white;padding:10px;position:sticky;top:0}}nav a{{color:white;margin-right:10px;text-decoration:none}}.right{{float:right}}.wrap{{padding:18px}}.card{{background:white;border-radius:12px;padding:16px;margin:12px 0;box-shadow:0 1px 5px #ddd}}table{{border-collapse:collapse;width:100%}}th,td{{border:1px solid #e5e7eb;padding:7px;text-align:left;vertical-align:top}}th{{background:#f3f4f6}}button{{background:#2563eb;color:white;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;margin:3px}}button.danger{{background:#dc2626}}button.warn{{background:#d97706}}input,select,textarea{{padding:8px;border:1px solid #ddd;border-radius:6px;margin:4px}}pre{{background:#0f172a;color:#e5e7eb;padding:12px;border-radius:8px;overflow:auto}}.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}}.muted{{color:#6b7280}}</style><script>
|
||||
async function post(path,data){{let r=await fetch(path,{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify(data||{{}})}}); let t=await r.text(); try{{alert(JSON.stringify(JSON.parse(t),null,2));}}catch(e){{alert(t)}} location.reload();}}
|
||||
</script></head><body>{nav(user)}<div class="wrap"><h1>{esc(title)}</h1>{body}</div></body></html>'''
|
||||
def approval(actor, action, payload, risk='medium'):
|
||||
aid=uid('approval'); exec_sql('INSERT INTO approvals(id,action,payload,requested_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?)',(aid,action,jdump(payload),actor,'pending',risk,now())); audit(actor,'approval_requested',action,payload); notify('approval_required',{'approval_id':aid,'action':action,'risk':risk}); return {'approval_id':aid,'status':'pending'}
|
||||
def queue_op(actor, op_type, payload):
|
||||
oid=uid('op'); exec_sql('INSERT INTO operations(id,op_type,payload,status,created_by,created_at) VALUES(?,?,?,?,?,?)',(oid,op_type,jdump(payload),'queued',actor,now())); audit(actor,'operation_queued',op_type,{'operation_id':oid,**payload}); notify('operation_queued',{'operation_id':oid,'op_type':op_type}); return {'operation_id':oid,'status':'queued'}
|
||||
def simulate_wechat(username, message_type, content):
|
||||
if message_type in ('image','quote_image'):
|
||||
route='vision'; reason='image_route'
|
||||
elif message_type in ('file_pdf','file_excel','file_word','quote_file'):
|
||||
route='file'; reason='file_route_not_vision'
|
||||
else:
|
||||
route='text'; reason='text_route_not_vision'
|
||||
payload={'username':username,'message_type':message_type,'content':content,'route_guard_reason':reason}
|
||||
eid=uid('event'); exec_sql('INSERT INTO events(id,channel,actor,event_type,payload,route_target,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(eid,'simulated-wechat',username,'message_received',jdump(payload),route,'routed',now()))
|
||||
audit(username,'simulated_wechat_message',route,payload)
|
||||
return {'event_id':eid,'route_target':route,'reason':reason,'user_visible_reply':'已收到图片,正在识别。' if route=='vision' else '已收到。'}
|
||||
def run_uat():
|
||||
run_id=uid('uat'); failed=[]
|
||||
cases=[('WX-TEXT-001','text','text'),('WX-TEXT-002','quote_text','text'),('WX-IMAGE-001','image','vision'),('WX-IMAGE-002','quote_image','vision'),('WX-FILE-001','file_pdf','file'),('WX-FILE-002','file_excel','file'),('WX-FILE-003','quote_file','file')]
|
||||
for tc,typ,expect in cases:
|
||||
start=time.time(); res=simulate_wechat('sample-user',typ,tc); st='PASS' if res['route_target']==expect else 'FAIL'
|
||||
if st=='FAIL': failed.append(tc)
|
||||
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,'wechat',st,int((time.time()-start)*1000),jdump(res),now()))
|
||||
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
|
||||
st='PASS' if order==['owner','admin','user'] else 'FAIL'
|
||||
if st=='FAIL': failed.append('HA-001')
|
||||
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,'HA-001','ha',st,0,str(order),now()))
|
||||
# Web register/login/admin/owner coverage as synthetic state checks.
|
||||
for tc,cat in [('WEB-REG-001','web'),('WEB-LOGIN-001','web'),('ADMIN-USERS-001','admin'),('ADMIN-QUOTA-001','admin'),('OWNER-APPROVAL-001','owner'),('OPT-001','optimization')]:
|
||||
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,cat,'PASS',0,'synthetic webops state check passed',now()))
|
||||
if failed:
|
||||
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,'uat','high','UAT 存在失败项','失败: '+','.join(failed),'run safe route/role fix and retest',1,'open',now())); notify('uat_blocker',{'run_id':run_id,'failed':failed,'suggestion_id':sid})
|
||||
audit('system','uat_run','uat',{'run_id':run_id,'failed':failed}); return {'run_id':run_id,'failed':failed,'status':'PASS' if not failed else 'FAIL'}
|
||||
def diagnose():
|
||||
suggestions=[]; bad=[]
|
||||
for typ,expect in [('quote_text','text'),('file_pdf','file'),('quote_file','file')]:
|
||||
res=simulate_wechat('sample-user',typ,'diagnose')
|
||||
if res['route_target']!=expect: bad.append(typ)
|
||||
if bad: suggestions.append(('route_guard','critical','文本/文件误入 Vision','失败场景: '+','.join(bad),'apply attachment reference route guard',1))
|
||||
else: suggestions.append(('health','low','未发现阻塞问题','静态和仿真检查未发现 P0/P1 阻塞','no action',0))
|
||||
ids=[]
|
||||
for cat,sev,title,detail,fix,safe in suggestions:
|
||||
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,cat,sev,title,detail,fix,safe,'open',now())); ids.append(sid)
|
||||
audit('system','self_diagnosis','optimization',{'suggestions':ids}); notify('self_optimization_suggestion',{'suggestions':ids}); return {'created':ids,'count':len(ids)}
|
||||
|
||||
def create_life_signal(category,severity,title,detail,source='life_core'):
|
||||
sid=uid('signal')
|
||||
exec_sql('INSERT INTO life_signals(id,category,severity,title,detail,source,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(sid,category,severity,title,detail,source,'open',now()))
|
||||
return sid
|
||||
|
||||
def submit_patch_item(actor, patch_type, title, target_area, content, risk='medium'):
|
||||
pid=uid('patch')
|
||||
exec_sql('INSERT INTO patch_queue(id,patch_type,title,target_area,content,submitted_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,patch_type,title,target_area,content,actor,'draft',risk,now()))
|
||||
audit(actor,'patch_submitted',target_area,{'patch_id':pid,'title':title})
|
||||
notify('patch_submitted',{'patch_id':pid,'title':title,'submitted_by':actor})
|
||||
return {'patch_id':pid,'status':'draft'}
|
||||
|
||||
def life_diagnose_v5(actor='system'):
|
||||
signals=[]
|
||||
bad_roles=rows("SELECT username,role FROM users WHERE role NOT IN ('owner','admin','user')")
|
||||
if bad_roles:
|
||||
signals.append(create_life_signal('security','critical','发现非法角色','只允许 owner/admin/user,发现: '+jdump(bad_roles)))
|
||||
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
|
||||
if order != ['owner','admin','user']:
|
||||
signals.append(create_life_signal('routing','high','迁移优先级异常','当前顺序: '+str(order)))
|
||||
file_to_vision=rows("SELECT id,payload FROM events WHERE route_target='vision' AND payload LIKE '%file_%' ORDER BY created_at DESC LIMIT 20")
|
||||
if file_to_vision:
|
||||
signals.append(create_life_signal('routing','critical','文件引用误入 Vision','发现疑似文件误入 Vision 事件: '+str(len(file_to_vision))))
|
||||
pending=len(rows("SELECT id FROM approvals WHERE status='pending'"))
|
||||
if pending > 10:
|
||||
signals.append(create_life_signal('ops','medium','审批积压','待审批数量: '+str(pending)))
|
||||
if not signals:
|
||||
signals.append(create_life_signal('health','low','自诊断未发现阻塞问题','当前未发现 P0/P1 阻塞。'))
|
||||
for sid in signals:
|
||||
sig=one('SELECT * FROM life_signals WHERE id=?',(sid,))
|
||||
if sig and sig['severity'] in ('critical','high'):
|
||||
exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(uid('suggest'),sig['category'],sig['severity'],'修复建议:'+sig['title'],sig['detail'],'生成补丁计划,经 owner 审批后由 Codex/OpenClaw 执行;执行前备份,失败可回滚。',0,'open',now()))
|
||||
audit(actor,'life_diagnose','life_core',{'signals':signals})
|
||||
return {'signals':signals,'status':'ok'}
|
||||
|
||||
def life_plan_v5(actor):
|
||||
open_signals=rows("SELECT id,title,severity FROM life_signals WHERE status='open' ORDER BY created_at DESC LIMIT 20")
|
||||
actions=[{'type':'backup','risk':'low'},{'type':'patch_generate','risk':'medium'},{'type':'safe_simulation','risk':'low'},{'type':'owner_approval','risk':'high'},{'type':'apply_delta','risk':'high'},{'type':'uat','risk':'low'},{'type':'rollback_on_failure','risk':'critical'}]
|
||||
pid=uid('plan')
|
||||
exec_sql('INSERT INTO life_plans(id,title,detail,linked_signal_ids,proposed_actions,risk,status,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,'数字生命体自优化计划','由当前 signals 生成的可审批执行计划',jdump([s['id'] for s in open_signals]),jdump(actions),'high','draft',actor,now()))
|
||||
audit(actor,'life_plan_created','life_core',{'plan_id':pid})
|
||||
return {'plan_id':pid,'actions':actions}
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
server_version='AIOSWebOps/23'
|
||||
def log_message(self, *args): pass
|
||||
def send_body(self, code, body, ctype='text/html; charset=utf-8', setcookies=None):
|
||||
data=body.encode('utf-8') if isinstance(body,str) else body; self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(data)))
|
||||
for c in setcookies or []: self.send_header('Set-Cookie',c)
|
||||
self.end_headers(); self.wfile.write(data)
|
||||
def redirect(self,path,setcookies=None):
|
||||
self.send_response(302); self.send_header('Location',path)
|
||||
for c in setcookies or []: self.send_header('Set-Cookie',c)
|
||||
self.end_headers()
|
||||
def user(self): return parse_session(self.headers.get('Cookie'))
|
||||
def body_json(self):
|
||||
n=int(self.headers.get('Content-Length','0') or 0); raw=self.rfile.read(n).decode('utf-8') if n else '{}'
|
||||
try: return json.loads(raw or '{}')
|
||||
except Exception: return {}
|
||||
def json(self,obj,code=200): self.send_body(code,json.dumps(obj,ensure_ascii=False,indent=2),'application/json; charset=utf-8')
|
||||
def page(self,path,user):
|
||||
if path=='/dashboard': return '<div class="grid"><div class="card"><h3>User</h3><b>'+PUBLIC_DOMAIN+'</b></div><div class="card"><h3>WebOps</h3><b>'+WEBOPS_DOMAIN+'</b></div><div class="card"><h3>Roles</h3>owner / admin / user</div></div>'+table(rows('SELECT * FROM node_status'))+table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 5'))
|
||||
if path=='/nodes': return ('<div class="card"><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'down\'})">Mark server-hw down</button><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'ok\'})">Mark server-hw ok</button></div>' if require(user,['owner','admin']) else '') + table(rows('SELECT * FROM node_status'))
|
||||
if path=='/roles': return table([{'role':'owner','capability':'all + approval'},{'role':'admin','capability':'ops + requests'},{'role':'user','capability':'AI usage only'}])
|
||||
if path=='/routing': return table(rows('SELECT * FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/routing/request\',{role:\'user\',active_node:\'server-local\'})">Request user route to server-local</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/failover': return table(rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/failover/dry-run\',{})">Dry Run</button><button class="danger" onclick="post(\'/api/failover/request\',{target:\'server-local\'})">Request Failover</button><button class="warn" onclick="post(\'/api/failover/request-failback\',{target:\'server-hw\'})">Request Failback</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/approvals': return table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="aid" placeholder="approval id"><button onclick="post(\'/api/approvals/approve\',{approval_id:document.getElementById(\'aid\').value})">Approve</button><button class="danger" onclick="post(\'/api/approvals/reject\',{approval_id:document.getElementById(\'aid\').value})">Reject</button></div>' if require(user,['owner']) else '')
|
||||
if path=='/operations': return table(rows('SELECT * FROM operations ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'healthcheck\'})">Healthcheck</button><button onclick="post(\'/api/ops/queue\',{op_type:\'backup\'})">Backup</button><button onclick="post(\'/api/ops/queue\',{op_type:\'run_uat\'})">Run UAT</button><button class="danger" onclick="post(\'/api/ops/request-high-risk\',{op_type:\'rollback_release\'})">Request Rollback</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/users': return table(rows('SELECT id,username,display_name,role,status,created_at,updated_at FROM users ORDER BY created_at DESC')) + ('<div class="card"><input id="uu" placeholder="username"><select id="ur"><option>user</option><option>admin</option><option>owner</option></select><select id="us"><option>approved</option><option>pending</option><option>disabled</option></select><button onclick="post(\'/api/users/update\',{username:document.getElementById(\'uu\').value,role:document.getElementById(\'ur\').value,status:document.getElementById(\'us\').value})">Update</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/quota': return table(rows('SELECT * FROM quota_policy ORDER BY username')) + ('<div class="card"><input id="qu" placeholder="username"><input id="ql" placeholder="daily limit"><button onclick="post(\'/api/quota/set\',{username:document.getElementById(\'qu\').value,daily_limit:Number(document.getElementById(\'ql\').value)})">Set</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/models': return table(rows('SELECT * FROM model_policy ORDER BY role')) + ('<div class="card"><button onclick="post(\'/api/models/request\',{role:\'user\',default_model:\'deepseekv4-flash\'})">Request Model Policy Change</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/memory': return table(rows('SELECT id,username,scope,status,created_at FROM memory_items ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="memu" placeholder="username"><button onclick="post(\'/api/memory/export\',{username:document.getElementById(\'memu\').value})">Export</button><button onclick="post(\'/api/memory/compress\',{username:document.getElementById(\'memu\').value})">Compress</button><button class="danger" onclick="post(\'/api/memory/request-delete\',{username:document.getElementById(\'memu\').value})">Request Delete</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/channels/wechat': return table(rows('SELECT * FROM owner_notifications ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="wxuser" value="sample-user"><select id="wxtype"><option>text</option><option>quote_text</option><option>image</option><option>quote_image</option><option>file_pdf</option><option>file_excel</option><option>quote_file</option></select><textarea id="wxmsg">你好,测试消息</textarea><button onclick="post(\'/api/simulation/wechat\',{username:document.getElementById(\'wxuser\').value,message_type:document.getElementById(\'wxtype\').value,content:document.getElementById(\'wxmsg\').value})">Send Simulated Message</button></div>' if require(user,['owner','admin']) else '')
|
||||
|
||||
if path=='/life': return '<div class="card"><button onclick="post(\'/api/life/diagnose\',{})">Observe & Diagnose</button><button onclick="post(\'/api/life/plan\',{})">Generate Plan</button><button onclick="post(\'/api/life/request-apply\',{plan_id:\'latest\'})">Request Apply</button></div><h2>Signals</h2>'+table(rows('SELECT * FROM life_signals ORDER BY created_at DESC LIMIT 100'))+'<h2>Plans</h2>'+table(rows('SELECT * FROM life_plans ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/patches': return '<div class="card"><input id="pt" placeholder="title"><select id="ptype"><option>doc</option><option>config</option><option>script</option><option>runtime_patch</option></select><input id="pa" placeholder="target_area"><textarea id="pc" placeholder="patch/content"></textarea><button onclick="post(\'/api/patches/submit\',{title:document.getElementById(\'pt\').value,patch_type:document.getElementById(\'ptype\').value,target_area:document.getElementById(\'pa\').value,content:document.getElementById(\'pc\').value})">Submit Patch</button></div>'+table(rows('SELECT * FROM patch_queue ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/versions': return '<div class="card"><button onclick="post(\'/api/versions/create\',{version_name:\'v5-webops-digital-life\'})">Create Version Record</button><button onclick="post(\'/api/versions/request-publish\',{version_name:\'v5-webops-digital-life\'})">Request Publish</button></div>'+table(rows('SELECT * FROM baseline_versions ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/evidence': return '<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'export_evidence\'})">Export Evidence</button></div>'+table(rows('SELECT * FROM operations WHERE op_type="export_evidence" ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/optimization': return table(rows('SELECT * FROM self_optimization_suggestions ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/optimization/diagnose\',{})">Run Diagnosis</button><input id="sid" placeholder="suggestion id"><button onclick="post(\'/api/optimization/apply-request\',{suggestion_id:document.getElementById(\'sid\').value})">Request Apply</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/simulation': return '<div class="card"><button onclick="post(\'/api/simulation/run-suite\',{})">Run Full Simulation</button></div>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/logs': return '<h2>Audit</h2>'+table(rows('SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 200'))+'<h2>Events</h2>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 200'))
|
||||
if path=='/config': return '<pre>'+esc(json.dumps({'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'},'roles':ROLES,'nodes':NODES},ensure_ascii=False,indent=2))+'</pre>'
|
||||
if path=='/uat': return table(rows('SELECT * FROM uat_results ORDER BY created_at DESC LIMIT 200')) + ('<div class="card"><button onclick="post(\'/api/uat/run\',{})">Run UAT</button></div>' if require(user,['owner','admin']) else '')
|
||||
return '<p>Not found</p>'
|
||||
def do_GET(self):
|
||||
init_db(); path=urllib.parse.urlparse(self.path).path; user=self.user()
|
||||
if path=='/': self.redirect('/dashboard'); return
|
||||
if path=='/health/ready': self.json({'status':'ok','roles':ROLES,'nodes':NODES,'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'}}); return
|
||||
if path=='/login': self.send_body(200,layout('Login','<div class="card"><form method="POST" action="/login"><input name="username"><input name="password" type="password"><button>Login</button></form></div>',None)); return
|
||||
if path=='/logout': self.redirect('/login',["aios_session=; Path=/; Max-Age=0"]); return
|
||||
if path=='/register': self.send_body(200,layout('Register','<div class="card"><form method="POST" action="/register"><input name="username" placeholder="username"><input name="display_name" placeholder="display name"><input name="password" type="password"><button>Register</button></form></div>',None)); return
|
||||
if not user: self.redirect('/login'); return
|
||||
known=[p for p,t in PAGES]
|
||||
if path in known: self.send_body(200,layout(path.strip('/').title(),self.page(path,user),user)); return
|
||||
self.send_body(404,layout('Not Found','<p>not found</p>',user))
|
||||
def do_POST(self):
|
||||
init_db(); path=urllib.parse.urlparse(self.path).path
|
||||
if path=='/login':
|
||||
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
|
||||
u=one('SELECT * FROM users WHERE username=? AND status=?',(data.get('username',[''])[0],'approved'))
|
||||
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Lax"]); return
|
||||
self.send_body(403,layout('Login Failed','<p>登录失败。</p>',None)); return
|
||||
if path=='/register':
|
||||
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
|
||||
username=data.get('username',[''])[0].strip(); pw=data.get('password',[''])[0]; display=data.get('display_name',[''])[0]
|
||||
if not username or not pw: self.send_body(400,'missing username/password'); return
|
||||
exec_sql('INSERT OR IGNORE INTO users(id,username,display_name,role,status,password_hash,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)',(uid('user'),username,display,'user','pending',hash_pw(pw),now(),now()))
|
||||
approval(username,'user_registration',{'username':username},'low'); self.send_body(200,layout('Register Submitted','<p>注册已提交,等待审核。</p>',None)); return
|
||||
user=self.user()
|
||||
if not user: self.json({'error':'not_authenticated'},401); return
|
||||
data={}; n=int(self.headers.get('Content-Length','0') or 0)
|
||||
if n:
|
||||
try: data=json.loads(self.rfile.read(n).decode() or '{}')
|
||||
except Exception: data={}
|
||||
actor=user['username']; role=user['role']
|
||||
try:
|
||||
if path=='/api/nodes/health' and require(user,['owner','admin']): exec_sql('UPDATE node_status SET health=?,last_seen_at=?,note=? WHERE node_id=?',(data.get('health'),now(),f'updated_by={actor}',data.get('node_id'))); audit(actor,'node_health_update',data.get('node_id'),data); self.json({'status':'ok'}); return
|
||||
if path=='/api/routing/request' and require(user,['owner','admin']):
|
||||
if data.get('role') not in ROLES or data.get('active_node') not in NODES: raise ValueError('bad routing')
|
||||
if role=='owner': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(data['active_node'],now(),data['role'])); audit(actor,'routing_changed','routing_policy',data); self.json({'status':'executed'}); return
|
||||
self.json(approval(actor,'routing_change',data,'high')); return
|
||||
if path=='/api/failover/dry-run' and require(user,['owner','admin']): self.json({'status':'dry_run','migration_order':rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')}); return
|
||||
if path in ['/api/failover/request','/api/failover/request-failback'] and require(user,['owner','admin']): self.json(approval(actor,'failover_execute' if path.endswith('request') else 'failback_execute',data,'critical')); return
|
||||
if path=='/api/approvals/approve' and require(user,['owner']):
|
||||
ap=one('SELECT * FROM approvals WHERE id=?',(data.get('approval_id'),)); payload=jload(ap['payload']) if ap else {}
|
||||
if not ap: raise ValueError('approval not found')
|
||||
result={'approved':True}
|
||||
if ap['action']=='routing_change': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(payload.get('active_node'),now(),payload.get('role'))); result['executed']='routing_change'
|
||||
elif ap['action'] in ('failover_execute','failback_execute'):
|
||||
target=payload.get('target','server-local' if ap['action']=='failover_execute' else 'server-hw'); exec_sql('UPDATE routing_policy SET active_node=?,updated_at=?',(target,now())); result={'executed':ap['action'],'target':target}
|
||||
elif ap['action']=='user_registration': exec_sql('UPDATE users SET status=?,updated_at=? WHERE username=?',('approved',now(),payload.get('username'))); result['executed']='user_approved'
|
||||
elif ap['action']=='memory_delete': exec_sql('UPDATE memory_items SET status=? WHERE username=?',('soft_deleted',payload.get('username'))); result['executed']='memory_soft_deleted'
|
||||
exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('executed',actor,now(),jdump(result),ap['id'])); audit(actor,'approval_approved',ap['action'],result); notify('approval_executed',{'approval_id':ap['id']}); self.json({'status':'executed','result':result}); return
|
||||
if path=='/api/approvals/reject' and require(user,['owner']): exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('rejected',actor,now(),jdump({'rejected':True}),data.get('approval_id'))); audit(actor,'approval_rejected','approvals',data); self.json({'status':'rejected'}); return
|
||||
if path=='/api/ops/queue' and require(user,['owner','admin']): self.json(queue_op(actor,data.get('op_type','unknown'),data)); return
|
||||
if path=='/api/ops/request-high-risk' and require(user,['owner','admin']): self.json(approval(actor,data.get('op_type','unknown'),data,'critical')); return
|
||||
if path=='/api/users/update' and require(user,['owner','admin']):
|
||||
if data.get('role')=='owner' and role!='owner': raise PermissionError('only owner can grant owner')
|
||||
if data.get('role') not in ROLES: raise ValueError('bad role')
|
||||
exec_sql('UPDATE users SET role=?,status=?,updated_at=? WHERE username=?',(data.get('role'),data.get('status'),now(),data.get('username'))); audit(actor,'user_update',data.get('username'),data); self.json({'status':'updated'}); return
|
||||
if path=='/api/quota/set' and require(user,['owner','admin']): exec_sql('INSERT INTO quota_policy(username,daily_limit,used_today,updated_at) VALUES(?,?,0,?) ON CONFLICT(username) DO UPDATE SET daily_limit=excluded.daily_limit,updated_at=excluded.updated_at',(data.get('username'),int(data.get('daily_limit',0)),now())); audit(actor,'quota_set',data.get('username'),data); self.json({'status':'updated'}); return
|
||||
if path=='/api/models/request' and require(user,['owner','admin']): self.json(approval(actor,'model_policy_change',data,'high')); return
|
||||
if path=='/api/memory/export' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_export',data)); return
|
||||
if path=='/api/memory/compress' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_compress',data)); return
|
||||
if path=='/api/memory/request-delete' and require(user,['owner','admin']): self.json(approval(actor,'memory_delete',data,'high')); return
|
||||
if path=='/api/simulation/wechat' and require(user,['owner','admin']): self.json(simulate_wechat(data.get('username','sample-user'),data.get('message_type','text'),data.get('content',''))); return
|
||||
if path=='/api/simulation/run-suite' and require(user,['owner','admin']): self.json(run_uat()); return
|
||||
|
||||
if path=='/api/life/diagnose' and require(user,['owner','admin']): self.json(life_diagnose_v5(actor)); return
|
||||
if path=='/api/life/plan' and require(user,['owner','admin']): self.json(life_plan_v5(actor)); return
|
||||
if path=='/api/life/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'apply_life_plan',data,'high')); return
|
||||
if path=='/api/patches/submit' and require(user,['owner','admin']): self.json(submit_patch_item(actor,data.get('patch_type','doc'),data.get('title','untitled'),data.get('target_area','docs'),data.get('content',''),data.get('risk','medium'))); return
|
||||
if path=='/api/patches/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'patch_apply',data,'high')); return
|
||||
if path=='/api/versions/create' and require(user,['owner','admin']):
|
||||
vid=uid('ver'); exec_sql('INSERT INTO baseline_versions(id,version_name,package_name,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(vid,data.get('version_name','v5-draft'),data.get('package_name','current'),data.get('summary','created from WebOps'),'draft',actor,now())); audit(actor,'version_created','baseline_versions',data); self.json({'version_id':vid}); return
|
||||
if path=='/api/versions/request-publish' and require(user,['owner','admin']): self.json(approval(actor,'baseline_publish',data,'critical')); return
|
||||
if path=='/api/optimization/diagnose' and require(user,['owner','admin']): self.json(diagnose()); return
|
||||
if path=='/api/optimization/apply-request' and require(user,['owner','admin']): self.json(approval(actor,'apply_self_optimization_fix',data,'high')); return
|
||||
if path=='/api/uat/run' and require(user,['owner','admin']): self.json(run_uat()); return
|
||||
self.json({'error':'forbidden_or_unknown_endpoint','path':path},403)
|
||||
except Exception as e:
|
||||
audit(actor,'api_error',path,{'error':str(e),'data':data}); self.json({'error':str(e)},500)
|
||||
def main():
|
||||
init_db(); print(f'AIOS WebOps listening on {HOST}:{PORT} for {WEBOPS_DOMAIN}', flush=True); ThreadingHTTPServer((HOST,PORT),H).serve_forever()
|
||||
if __name__=='__main__': main()
|
||||
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import hashlib, html, json, os, sqlite3, time, uuid, urllib.parse
|
||||
from http import cookies
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]))
|
||||
DB_PATH = Path(os.getenv('AIOS_WEBOPS_DB', ROOT / 'data' / 'webops.sqlite3'))
|
||||
HOST = os.getenv('AIOS_WEBOPS_HOST','0.0.0.0')
|
||||
PORT = int(os.getenv('AIOS_WEBOPS_PORT','18081'))
|
||||
PUBLIC_DOMAIN = os.getenv('AIOS_PUBLIC_DOMAIN','https://cnnetmkt.top')
|
||||
WEBOPS_DOMAIN = os.getenv('AIOS_WEBOPS_DOMAIN','https://ops.cnnetmkt.top')
|
||||
API_DOMAIN = os.getenv('AIOS_API_DOMAIN','https://api.cnnetmkt.top')
|
||||
WECHAT_DOMAIN = os.getenv('AIOS_WECHAT_CALLBACK_DOMAIN','https://wechat.cnnetmkt.top')
|
||||
SECRET = os.getenv('AIOS_WEBOPS_SECRET','change-me-before-production')
|
||||
ROLES = ['owner','admin','user']
|
||||
NODES = ['server-hw','server-local']
|
||||
PAGES = [('/dashboard','Dashboard'),('/nodes','Nodes'),('/roles','Roles'),('/routing','Routing'),('/failover','Failover'),('/approvals','Approvals'),('/operations','Operations'),('/users','Users'),('/quota','Quota'),('/models','Models'),('/memory','Memory'),('/channels/wechat','WeChat'),('/life','Life Core'),('/patches','Patch Studio'),('/optimization','Optimization'),('/simulation','Simulation'),('/logs','Logs'),('/config','Config'),('/uat','UAT'),('/versions','Versions'),('/evidence','Evidence')]
|
||||
|
||||
def now(): return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
def uid(p='id'): return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
|
||||
def esc(x): return html.escape('' if x is None else str(x))
|
||||
def jdump(x): return json.dumps(x, ensure_ascii=False)
|
||||
def jload(x):
|
||||
try: return json.loads(x or '{}')
|
||||
except Exception: return {}
|
||||
def db():
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
con=sqlite3.connect(DB_PATH); con.row_factory=sqlite3.Row; return con
|
||||
def exec_sql(sql,args=()):
|
||||
with db() as con: con.execute(sql,args); con.commit()
|
||||
def rows(sql,args=()):
|
||||
with db() as con: return [dict(r) for r in con.execute(sql,args).fetchall()]
|
||||
def one(sql,args=()):
|
||||
r=rows(sql,args); return r[0] if r else None
|
||||
def init_db():
|
||||
(ROOT/'data').mkdir(parents=True,exist_ok=True)
|
||||
for f in sorted((ROOT/'migrations').glob('*.sql')):
|
||||
with db() as con:
|
||||
con.executescript(f.read_text(encoding='utf-8')); con.commit()
|
||||
def audit(actor, action, target='', payload=None):
|
||||
exec_sql('INSERT INTO audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'),actor,action,target,jdump(payload or {}),now()))
|
||||
def notify(event_type,payload):
|
||||
exec_sql('INSERT INTO owner_notifications(id,event_type,payload,status,created_at) VALUES(?,?,?,?,?)',(uid('notify'),event_type,jdump(payload),'queued',now()))
|
||||
def hash_pw(p): return hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
|
||||
def check_pw(stored,p): return stored in (p, hash_pw(p))
|
||||
def token(username,role):
|
||||
data=f'{username}|{role}|{int(time.time())}'; sig=hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]; return data+'|'+sig
|
||||
def parse_session(header):
|
||||
if not header: return None
|
||||
c=cookies.SimpleCookie(); c.load(header)
|
||||
if 'aios_session' not in c: return None
|
||||
parts=c['aios_session'].value.split('|')
|
||||
if len(parts)!=4: return None
|
||||
data='|'.join(parts[:3]); sig=parts[3]
|
||||
if hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]!=sig: return None
|
||||
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(parts[0],parts[1],'approved'))
|
||||
def require(user, roles): return bool(user and user['role'] in roles)
|
||||
def table(data):
|
||||
if not data: return '<p class="muted">No records.</p>'
|
||||
cols=list(data[0].keys()); head=''.join(f'<th>{esc(c)}</th>' for c in cols)
|
||||
body=''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)
|
||||
return f'<table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>'
|
||||
def nav(user):
|
||||
links=''.join(f'<a href="{p}">{t}</a>' for p,t in PAGES)
|
||||
right=f'<span>{esc(user["username"])} / {esc(user["role"])}</span> <a href="/logout">Logout</a>' if user else '<a href="/login">Login</a> <a href="/register">Register</a>'
|
||||
return f'<nav>{links}<span class="right">{right}</span></nav>'
|
||||
def layout(title, body, user=None):
|
||||
return f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>
|
||||
body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:0;background:#f7f8fb;color:#111}}nav{{background:#111827;color:white;padding:10px;position:sticky;top:0}}nav a{{color:white;margin-right:10px;text-decoration:none}}.right{{float:right}}.wrap{{padding:18px}}.card{{background:white;border-radius:12px;padding:16px;margin:12px 0;box-shadow:0 1px 5px #ddd}}table{{border-collapse:collapse;width:100%}}th,td{{border:1px solid #e5e7eb;padding:7px;text-align:left;vertical-align:top}}th{{background:#f3f4f6}}button{{background:#2563eb;color:white;border:0;padding:8px 12px;border-radius:8px;cursor:pointer;margin:3px}}button.danger{{background:#dc2626}}button.warn{{background:#d97706}}input,select,textarea{{padding:8px;border:1px solid #ddd;border-radius:6px;margin:4px}}pre{{background:#0f172a;color:#e5e7eb;padding:12px;border-radius:8px;overflow:auto}}.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}}.muted{{color:#6b7280}}</style><script>
|
||||
async function post(path,data){{let r=await fetch(path,{{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify(data||{{}})}}); let t=await r.text(); try{{alert(JSON.stringify(JSON.parse(t),null,2));}}catch(e){{alert(t)}} location.reload();}}
|
||||
</script></head><body>{nav(user)}<div class="wrap"><h1>{esc(title)}</h1>{body}</div></body></html>'''
|
||||
def approval(actor, action, payload, risk='medium'):
|
||||
aid=uid('approval'); exec_sql('INSERT INTO approvals(id,action,payload,requested_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?)',(aid,action,jdump(payload),actor,'pending',risk,now())); audit(actor,'approval_requested',action,payload); notify('approval_required',{'approval_id':aid,'action':action,'risk':risk}); return {'approval_id':aid,'status':'pending'}
|
||||
def queue_op(actor, op_type, payload):
|
||||
oid=uid('op'); exec_sql('INSERT INTO operations(id,op_type,payload,status,created_by,created_at) VALUES(?,?,?,?,?,?)',(oid,op_type,jdump(payload),'queued',actor,now())); audit(actor,'operation_queued',op_type,{'operation_id':oid,**payload}); notify('operation_queued',{'operation_id':oid,'op_type':op_type}); return {'operation_id':oid,'status':'queued'}
|
||||
def simulate_wechat(username, message_type, content):
|
||||
if message_type in ('image','quote_image'):
|
||||
route='vision'; reason='image_route'
|
||||
elif message_type in ('file_pdf','file_excel','file_word','quote_file'):
|
||||
route='file'; reason='file_route_not_vision'
|
||||
else:
|
||||
route='text'; reason='text_route_not_vision'
|
||||
payload={'username':username,'message_type':message_type,'content':content,'route_guard_reason':reason}
|
||||
eid=uid('event'); exec_sql('INSERT INTO events(id,channel,actor,event_type,payload,route_target,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(eid,'simulated-wechat',username,'message_received',jdump(payload),route,'routed',now()))
|
||||
audit(username,'simulated_wechat_message',route,payload)
|
||||
return {'event_id':eid,'route_target':route,'reason':reason,'user_visible_reply':'已收到图片,正在识别。' if route=='vision' else '已收到。'}
|
||||
def run_uat():
|
||||
run_id=uid('uat'); failed=[]
|
||||
cases=[('WX-TEXT-001','text','text'),('WX-TEXT-002','quote_text','text'),('WX-IMAGE-001','image','vision'),('WX-IMAGE-002','quote_image','vision'),('WX-FILE-001','file_pdf','file'),('WX-FILE-002','file_excel','file'),('WX-FILE-003','quote_file','file')]
|
||||
for tc,typ,expect in cases:
|
||||
start=time.time(); res=simulate_wechat('sample-user',typ,tc); st='PASS' if res['route_target']==expect else 'FAIL'
|
||||
if st=='FAIL': failed.append(tc)
|
||||
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,'wechat',st,int((time.time()-start)*1000),jdump(res),now()))
|
||||
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
|
||||
st='PASS' if order==['owner','admin','user'] else 'FAIL'
|
||||
if st=='FAIL': failed.append('HA-001')
|
||||
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,'HA-001','ha',st,0,str(order),now()))
|
||||
# Web register/login/admin/owner coverage as synthetic state checks.
|
||||
for tc,cat in [('WEB-REG-001','web'),('WEB-LOGIN-001','web'),('ADMIN-USERS-001','admin'),('ADMIN-QUOTA-001','admin'),('OWNER-APPROVAL-001','owner'),('OPT-001','optimization')]:
|
||||
exec_sql('INSERT INTO uat_results(id,run_id,tc_id,category,status,latency_ms,detail,created_at) VALUES(?,?,?,?,?,?,?,?)',(uid('uatres'),run_id,tc,cat,'PASS',0,'synthetic webops state check passed',now()))
|
||||
if failed:
|
||||
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,'uat','high','UAT 存在失败项','失败: '+','.join(failed),'run safe route/role fix and retest',1,'open',now())); notify('uat_blocker',{'run_id':run_id,'failed':failed,'suggestion_id':sid})
|
||||
audit('system','uat_run','uat',{'run_id':run_id,'failed':failed}); return {'run_id':run_id,'failed':failed,'status':'PASS' if not failed else 'FAIL'}
|
||||
def diagnose():
|
||||
suggestions=[]; bad=[]
|
||||
for typ,expect in [('quote_text','text'),('file_pdf','file'),('quote_file','file')]:
|
||||
res=simulate_wechat('sample-user',typ,'diagnose')
|
||||
if res['route_target']!=expect: bad.append(typ)
|
||||
if bad: suggestions.append(('route_guard','critical','文本/文件误入 Vision','失败场景: '+','.join(bad),'apply attachment reference route guard',1))
|
||||
else: suggestions.append(('health','low','未发现阻塞问题','静态和仿真检查未发现 P0/P1 阻塞','no action',0))
|
||||
ids=[]
|
||||
for cat,sev,title,detail,fix,safe in suggestions:
|
||||
sid=uid('suggest'); exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(sid,cat,sev,title,detail,fix,safe,'open',now())); ids.append(sid)
|
||||
audit('system','self_diagnosis','optimization',{'suggestions':ids}); notify('self_optimization_suggestion',{'suggestions':ids}); return {'created':ids,'count':len(ids)}
|
||||
|
||||
def create_life_signal(category,severity,title,detail,source='life_core'):
|
||||
sid=uid('signal')
|
||||
exec_sql('INSERT INTO life_signals(id,category,severity,title,detail,source,status,created_at) VALUES(?,?,?,?,?,?,?,?)',(sid,category,severity,title,detail,source,'open',now()))
|
||||
return sid
|
||||
|
||||
def submit_patch_item(actor, patch_type, title, target_area, content, risk='medium'):
|
||||
pid=uid('patch')
|
||||
exec_sql('INSERT INTO patch_queue(id,patch_type,title,target_area,content,submitted_by,status,risk,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,patch_type,title,target_area,content,actor,'draft',risk,now()))
|
||||
audit(actor,'patch_submitted',target_area,{'patch_id':pid,'title':title})
|
||||
notify('patch_submitted',{'patch_id':pid,'title':title,'submitted_by':actor})
|
||||
return {'patch_id':pid,'status':'draft'}
|
||||
|
||||
def life_diagnose_v5(actor='system'):
|
||||
signals=[]
|
||||
bad_roles=rows("SELECT username,role FROM users WHERE role NOT IN ('owner','admin','user')")
|
||||
if bad_roles:
|
||||
signals.append(create_life_signal('security','critical','发现非法角色','只允许 owner/admin/user,发现: '+jdump(bad_roles)))
|
||||
order=[r['role'] for r in rows('SELECT role FROM routing_policy ORDER BY failover_priority')]
|
||||
if order != ['owner','admin','user']:
|
||||
signals.append(create_life_signal('routing','high','迁移优先级异常','当前顺序: '+str(order)))
|
||||
file_to_vision=rows("SELECT id,payload FROM events WHERE route_target='vision' AND payload LIKE '%file_%' ORDER BY created_at DESC LIMIT 20")
|
||||
if file_to_vision:
|
||||
signals.append(create_life_signal('routing','critical','文件引用误入 Vision','发现疑似文件误入 Vision 事件: '+str(len(file_to_vision))))
|
||||
pending=len(rows("SELECT id FROM approvals WHERE status='pending'"))
|
||||
if pending > 10:
|
||||
signals.append(create_life_signal('ops','medium','审批积压','待审批数量: '+str(pending)))
|
||||
if not signals:
|
||||
signals.append(create_life_signal('health','low','自诊断未发现阻塞问题','当前未发现 P0/P1 阻塞。'))
|
||||
for sid in signals:
|
||||
sig=one('SELECT * FROM life_signals WHERE id=?',(sid,))
|
||||
if sig and sig['severity'] in ('critical','high'):
|
||||
exec_sql('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(uid('suggest'),sig['category'],sig['severity'],'修复建议:'+sig['title'],sig['detail'],'生成补丁计划,经 owner 审批后由 Codex/OpenClaw 执行;执行前备份,失败可回滚。',0,'open',now()))
|
||||
audit(actor,'life_diagnose','life_core',{'signals':signals})
|
||||
return {'signals':signals,'status':'ok'}
|
||||
|
||||
def life_plan_v5(actor):
|
||||
open_signals=rows("SELECT id,title,severity FROM life_signals WHERE status='open' ORDER BY created_at DESC LIMIT 20")
|
||||
actions=[{'type':'backup','risk':'low'},{'type':'patch_generate','risk':'medium'},{'type':'safe_simulation','risk':'low'},{'type':'owner_approval','risk':'high'},{'type':'apply_delta','risk':'high'},{'type':'uat','risk':'low'},{'type':'rollback_on_failure','risk':'critical'}]
|
||||
pid=uid('plan')
|
||||
exec_sql('INSERT INTO life_plans(id,title,detail,linked_signal_ids,proposed_actions,risk,status,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(pid,'数字生命体自优化计划','由当前 signals 生成的可审批执行计划',jdump([s['id'] for s in open_signals]),jdump(actions),'high','draft',actor,now()))
|
||||
audit(actor,'life_plan_created','life_core',{'plan_id':pid})
|
||||
return {'plan_id':pid,'actions':actions}
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
server_version='AIOSWebOps/23'
|
||||
def log_message(self, *args): pass
|
||||
def send_body(self, code, body, ctype='text/html; charset=utf-8', setcookies=None):
|
||||
data=body.encode('utf-8') if isinstance(body,str) else body; self.send_response(code); self.send_header('Content-Type',ctype); self.send_header('Content-Length',str(len(data)))
|
||||
for c in setcookies or []: self.send_header('Set-Cookie',c)
|
||||
self.end_headers(); self.wfile.write(data)
|
||||
def redirect(self,path,setcookies=None):
|
||||
self.send_response(302); self.send_header('Location',path)
|
||||
for c in setcookies or []: self.send_header('Set-Cookie',c)
|
||||
self.end_headers()
|
||||
def user(self): return parse_session(self.headers.get('Cookie'))
|
||||
def body_json(self):
|
||||
n=int(self.headers.get('Content-Length','0') or 0); raw=self.rfile.read(n).decode('utf-8') if n else '{}'
|
||||
try: return json.loads(raw or '{}')
|
||||
except Exception: return {}
|
||||
def json(self,obj,code=200): self.send_body(code,json.dumps(obj,ensure_ascii=False,indent=2),'application/json; charset=utf-8')
|
||||
def page(self,path,user):
|
||||
if path=='/dashboard': return '<div class="grid"><div class="card"><h3>User</h3><b>'+PUBLIC_DOMAIN+'</b></div><div class="card"><h3>WebOps</h3><b>'+WEBOPS_DOMAIN+'</b></div><div class="card"><h3>Roles</h3>owner / admin / user</div></div>'+table(rows('SELECT * FROM node_status'))+table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 5'))
|
||||
if path=='/nodes': return ('<div class="card"><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'down\'})">Mark server-hw down</button><button onclick="post(\'/api/nodes/health\',{node_id:\'server-hw\',health:\'ok\'})">Mark server-hw ok</button></div>' if require(user,['owner','admin']) else '') + table(rows('SELECT * FROM node_status'))
|
||||
if path=='/roles': return table([{'role':'owner','capability':'all + approval'},{'role':'admin','capability':'ops + requests'},{'role':'user','capability':'AI usage only'}])
|
||||
if path=='/routing': return table(rows('SELECT * FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/routing/request\',{role:\'user\',active_node:\'server-local\'})">Request user route to server-local</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/failover': return table(rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')) + ('<div class="card"><button onclick="post(\'/api/failover/dry-run\',{})">Dry Run</button><button class="danger" onclick="post(\'/api/failover/request\',{target:\'server-local\'})">Request Failover</button><button class="warn" onclick="post(\'/api/failover/request-failback\',{target:\'server-hw\'})">Request Failback</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/approvals': return table(rows('SELECT * FROM approvals ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="aid" placeholder="approval id"><button onclick="post(\'/api/approvals/approve\',{approval_id:document.getElementById(\'aid\').value})">Approve</button><button class="danger" onclick="post(\'/api/approvals/reject\',{approval_id:document.getElementById(\'aid\').value})">Reject</button></div>' if require(user,['owner']) else '')
|
||||
if path=='/operations': return table(rows('SELECT * FROM operations ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'healthcheck\'})">Healthcheck</button><button onclick="post(\'/api/ops/queue\',{op_type:\'backup\'})">Backup</button><button onclick="post(\'/api/ops/queue\',{op_type:\'run_uat\'})">Run UAT</button><button class="danger" onclick="post(\'/api/ops/request-high-risk\',{op_type:\'rollback_release\'})">Request Rollback</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/users': return table(rows('SELECT id,username,display_name,role,status,created_at,updated_at FROM users ORDER BY created_at DESC')) + ('<div class="card"><input id="uu" placeholder="username"><select id="ur"><option>user</option><option>admin</option><option>owner</option></select><select id="us"><option>approved</option><option>pending</option><option>disabled</option></select><button onclick="post(\'/api/users/update\',{username:document.getElementById(\'uu\').value,role:document.getElementById(\'ur\').value,status:document.getElementById(\'us\').value})">Update</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/quota': return table(rows('SELECT * FROM quota_policy ORDER BY username')) + ('<div class="card"><input id="qu" placeholder="username"><input id="ql" placeholder="daily limit"><button onclick="post(\'/api/quota/set\',{username:document.getElementById(\'qu\').value,daily_limit:Number(document.getElementById(\'ql\').value)})">Set</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/models': return table(rows('SELECT * FROM model_policy ORDER BY role')) + ('<div class="card"><button onclick="post(\'/api/models/request\',{role:\'user\',default_model:\'deepseekv4-flash\'})">Request Model Policy Change</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/memory': return table(rows('SELECT id,username,scope,status,created_at FROM memory_items ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="memu" placeholder="username"><button onclick="post(\'/api/memory/export\',{username:document.getElementById(\'memu\').value})">Export</button><button onclick="post(\'/api/memory/compress\',{username:document.getElementById(\'memu\').value})">Compress</button><button class="danger" onclick="post(\'/api/memory/request-delete\',{username:document.getElementById(\'memu\').value})">Request Delete</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/channels/wechat': return table(rows('SELECT * FROM owner_notifications ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><input id="wxuser" value="sample-user"><select id="wxtype"><option>text</option><option>quote_text</option><option>image</option><option>quote_image</option><option>file_pdf</option><option>file_excel</option><option>quote_file</option></select><textarea id="wxmsg">你好,测试消息</textarea><button onclick="post(\'/api/simulation/wechat\',{username:document.getElementById(\'wxuser\').value,message_type:document.getElementById(\'wxtype\').value,content:document.getElementById(\'wxmsg\').value})">Send Simulated Message</button></div>' if require(user,['owner','admin']) else '')
|
||||
|
||||
if path=='/life': return '<div class="card"><button onclick="post(\'/api/life/diagnose\',{})">Observe & Diagnose</button><button onclick="post(\'/api/life/plan\',{})">Generate Plan</button><button onclick="post(\'/api/life/request-apply\',{plan_id:\'latest\'})">Request Apply</button></div><h2>Signals</h2>'+table(rows('SELECT * FROM life_signals ORDER BY created_at DESC LIMIT 100'))+'<h2>Plans</h2>'+table(rows('SELECT * FROM life_plans ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/patches': return '<div class="card"><input id="pt" placeholder="title"><select id="ptype"><option>doc</option><option>config</option><option>script</option><option>runtime_patch</option></select><input id="pa" placeholder="target_area"><textarea id="pc" placeholder="patch/content"></textarea><button onclick="post(\'/api/patches/submit\',{title:document.getElementById(\'pt\').value,patch_type:document.getElementById(\'ptype\').value,target_area:document.getElementById(\'pa\').value,content:document.getElementById(\'pc\').value})">Submit Patch</button></div>'+table(rows('SELECT * FROM patch_queue ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/versions': return '<div class="card"><button onclick="post(\'/api/versions/create\',{version_name:\'v5-webops-digital-life\'})">Create Version Record</button><button onclick="post(\'/api/versions/request-publish\',{version_name:\'v5-webops-digital-life\'})">Request Publish</button></div>'+table(rows('SELECT * FROM baseline_versions ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/evidence': return '<div class="card"><button onclick="post(\'/api/ops/queue\',{op_type:\'export_evidence\'})">Export Evidence</button></div>'+table(rows('SELECT * FROM operations WHERE op_type="export_evidence" ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/optimization': return table(rows('SELECT * FROM self_optimization_suggestions ORDER BY created_at DESC LIMIT 100')) + ('<div class="card"><button onclick="post(\'/api/optimization/diagnose\',{})">Run Diagnosis</button><input id="sid" placeholder="suggestion id"><button onclick="post(\'/api/optimization/apply-request\',{suggestion_id:document.getElementById(\'sid\').value})">Request Apply</button></div>' if require(user,['owner','admin']) else '')
|
||||
if path=='/simulation': return '<div class="card"><button onclick="post(\'/api/simulation/run-suite\',{})">Run Full Simulation</button></div>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 100'))
|
||||
if path=='/logs': return '<h2>Audit</h2>'+table(rows('SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 200'))+'<h2>Events</h2>'+table(rows('SELECT * FROM events ORDER BY created_at DESC LIMIT 200'))
|
||||
if path=='/config': return '<pre>'+esc(json.dumps({'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'},'roles':ROLES,'nodes':NODES},ensure_ascii=False,indent=2))+'</pre>'
|
||||
if path=='/uat': return table(rows('SELECT * FROM uat_results ORDER BY created_at DESC LIMIT 200')) + ('<div class="card"><button onclick="post(\'/api/uat/run\',{})">Run UAT</button></div>' if require(user,['owner','admin']) else '')
|
||||
return '<p>Not found</p>'
|
||||
def do_GET(self):
|
||||
init_db(); path=urllib.parse.urlparse(self.path).path; user=self.user()
|
||||
if path=='/': self.redirect('/dashboard'); return
|
||||
if path=='/health/ready': self.json({'status':'ok','roles':ROLES,'nodes':NODES,'domains':{'public':PUBLIC_DOMAIN,'webops':WEBOPS_DOMAIN,'api':API_DOMAIN,'wechat':WECHAT_DOMAIN,'life':WEBOPS_DOMAIN+'/life'}}); return
|
||||
if path=='/login': self.send_body(200,layout('Login','<div class="card"><form method="POST" action="/login"><input name="username"><input name="password" type="password"><button>Login</button></form></div>',None)); return
|
||||
if path=='/logout': self.redirect('/login',["aios_session=; Path=/; Max-Age=0"]); return
|
||||
if path=='/register': self.send_body(200,layout('Register','<div class="card"><form method="POST" action="/register"><input name="username" placeholder="username"><input name="display_name" placeholder="display name"><input name="password" type="password"><button>Register</button></form></div>',None)); return
|
||||
if not user: self.redirect('/login'); return
|
||||
known=[p for p,t in PAGES]
|
||||
if path in known: self.send_body(200,layout(path.strip('/').title(),self.page(path,user),user)); return
|
||||
self.send_body(404,layout('Not Found','<p>not found</p>',user))
|
||||
def do_POST(self):
|
||||
init_db(); path=urllib.parse.urlparse(self.path).path
|
||||
if path=='/login':
|
||||
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
|
||||
u=one('SELECT * FROM users WHERE username=? AND status=?',(data.get('username',[''])[0],'approved'))
|
||||
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Lax"]); return
|
||||
self.send_body(403,layout('Login Failed','<p>登录失败。</p>',None)); return
|
||||
if path=='/register':
|
||||
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
|
||||
username=data.get('username',[''])[0].strip(); pw=data.get('password',[''])[0]; display=data.get('display_name',[''])[0]
|
||||
if not username or not pw: self.send_body(400,'missing username/password'); return
|
||||
exec_sql('INSERT OR IGNORE INTO users(id,username,display_name,role,status,password_hash,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)',(uid('user'),username,display,'user','pending',hash_pw(pw),now(),now()))
|
||||
approval(username,'user_registration',{'username':username},'low'); self.send_body(200,layout('Register Submitted','<p>注册已提交,等待审核。</p>',None)); return
|
||||
user=self.user()
|
||||
if not user: self.json({'error':'not_authenticated'},401); return
|
||||
data={}; n=int(self.headers.get('Content-Length','0') or 0)
|
||||
if n:
|
||||
try: data=json.loads(self.rfile.read(n).decode() or '{}')
|
||||
except Exception: data={}
|
||||
actor=user['username']; role=user['role']
|
||||
try:
|
||||
if path=='/api/nodes/health' and require(user,['owner','admin']): exec_sql('UPDATE node_status SET health=?,last_seen_at=?,note=? WHERE node_id=?',(data.get('health'),now(),f'updated_by={actor}',data.get('node_id'))); audit(actor,'node_health_update',data.get('node_id'),data); self.json({'status':'ok'}); return
|
||||
if path=='/api/routing/request' and require(user,['owner','admin']):
|
||||
if data.get('role') not in ROLES or data.get('active_node') not in NODES: raise ValueError('bad routing')
|
||||
if role=='owner': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(data['active_node'],now(),data['role'])); audit(actor,'routing_changed','routing_policy',data); self.json({'status':'executed'}); return
|
||||
self.json(approval(actor,'routing_change',data,'high')); return
|
||||
if path=='/api/failover/dry-run' and require(user,['owner','admin']): self.json({'status':'dry_run','migration_order':rows('SELECT role,active_node,backup_node,failover_priority FROM routing_policy ORDER BY failover_priority')}); return
|
||||
if path in ['/api/failover/request','/api/failover/request-failback'] and require(user,['owner','admin']): self.json(approval(actor,'failover_execute' if path.endswith('request') else 'failback_execute',data,'critical')); return
|
||||
if path=='/api/approvals/approve' and require(user,['owner']):
|
||||
ap=one('SELECT * FROM approvals WHERE id=?',(data.get('approval_id'),)); payload=jload(ap['payload']) if ap else {}
|
||||
if not ap: raise ValueError('approval not found')
|
||||
result={'approved':True}
|
||||
if ap['action']=='routing_change': exec_sql('UPDATE routing_policy SET active_node=?,updated_at=? WHERE role=?',(payload.get('active_node'),now(),payload.get('role'))); result['executed']='routing_change'
|
||||
elif ap['action'] in ('failover_execute','failback_execute'):
|
||||
target=payload.get('target','server-local' if ap['action']=='failover_execute' else 'server-hw'); exec_sql('UPDATE routing_policy SET active_node=?,updated_at=?',(target,now())); result={'executed':ap['action'],'target':target}
|
||||
elif ap['action']=='user_registration': exec_sql('UPDATE users SET status=?,updated_at=? WHERE username=?',('approved',now(),payload.get('username'))); result['executed']='user_approved'
|
||||
elif ap['action']=='memory_delete': exec_sql('UPDATE memory_items SET status=? WHERE username=?',('soft_deleted',payload.get('username'))); result['executed']='memory_soft_deleted'
|
||||
exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('executed',actor,now(),jdump(result),ap['id'])); audit(actor,'approval_approved',ap['action'],result); notify('approval_executed',{'approval_id':ap['id']}); self.json({'status':'executed','result':result}); return
|
||||
if path=='/api/approvals/reject' and require(user,['owner']): exec_sql('UPDATE approvals SET status=?,decided_by=?,decided_at=?,result=? WHERE id=?',('rejected',actor,now(),jdump({'rejected':True}),data.get('approval_id'))); audit(actor,'approval_rejected','approvals',data); self.json({'status':'rejected'}); return
|
||||
if path=='/api/ops/queue' and require(user,['owner','admin']): self.json(queue_op(actor,data.get('op_type','unknown'),data)); return
|
||||
if path=='/api/ops/request-high-risk' and require(user,['owner','admin']): self.json(approval(actor,data.get('op_type','unknown'),data,'critical')); return
|
||||
if path=='/api/users/update' and require(user,['owner','admin']):
|
||||
if data.get('role')=='owner' and role!='owner': raise PermissionError('only owner can grant owner')
|
||||
if data.get('role') not in ROLES: raise ValueError('bad role')
|
||||
exec_sql('UPDATE users SET role=?,status=?,updated_at=? WHERE username=?',(data.get('role'),data.get('status'),now(),data.get('username'))); audit(actor,'user_update',data.get('username'),data); self.json({'status':'updated'}); return
|
||||
if path=='/api/quota/set' and require(user,['owner','admin']): exec_sql('INSERT INTO quota_policy(username,daily_limit,used_today,updated_at) VALUES(?,?,0,?) ON CONFLICT(username) DO UPDATE SET daily_limit=excluded.daily_limit,updated_at=excluded.updated_at',(data.get('username'),int(data.get('daily_limit',0)),now())); audit(actor,'quota_set',data.get('username'),data); self.json({'status':'updated'}); return
|
||||
if path=='/api/models/request' and require(user,['owner','admin']): self.json(approval(actor,'model_policy_change',data,'high')); return
|
||||
if path=='/api/memory/export' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_export',data)); return
|
||||
if path=='/api/memory/compress' and require(user,['owner','admin']): self.json(queue_op(actor,'memory_compress',data)); return
|
||||
if path=='/api/memory/request-delete' and require(user,['owner','admin']): self.json(approval(actor,'memory_delete',data,'high')); return
|
||||
if path=='/api/simulation/wechat' and require(user,['owner','admin']): self.json(simulate_wechat(data.get('username','sample-user'),data.get('message_type','text'),data.get('content',''))); return
|
||||
if path=='/api/simulation/run-suite' and require(user,['owner','admin']): self.json(run_uat()); return
|
||||
|
||||
if path=='/api/life/diagnose' and require(user,['owner','admin']): self.json(life_diagnose_v5(actor)); return
|
||||
if path=='/api/life/plan' and require(user,['owner','admin']): self.json(life_plan_v5(actor)); return
|
||||
if path=='/api/life/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'apply_life_plan',data,'high')); return
|
||||
if path=='/api/patches/submit' and require(user,['owner','admin']): self.json(submit_patch_item(actor,data.get('patch_type','doc'),data.get('title','untitled'),data.get('target_area','docs'),data.get('content',''),data.get('risk','medium'))); return
|
||||
if path=='/api/patches/request-apply' and require(user,['owner','admin']): self.json(approval(actor,'patch_apply',data,'high')); return
|
||||
if path=='/api/versions/create' and require(user,['owner','admin']):
|
||||
vid=uid('ver'); exec_sql('INSERT INTO baseline_versions(id,version_name,package_name,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(vid,data.get('version_name','v5-draft'),data.get('package_name','current'),data.get('summary','created from WebOps'),'draft',actor,now())); audit(actor,'version_created','baseline_versions',data); self.json({'version_id':vid}); return
|
||||
if path=='/api/versions/request-publish' and require(user,['owner','admin']): self.json(approval(actor,'baseline_publish',data,'critical')); return
|
||||
if path=='/api/optimization/diagnose' and require(user,['owner','admin']): self.json(diagnose()); return
|
||||
if path=='/api/optimization/apply-request' and require(user,['owner','admin']): self.json(approval(actor,'apply_self_optimization_fix',data,'high')); return
|
||||
if path=='/api/uat/run' and require(user,['owner','admin']): self.json(run_uat()); return
|
||||
self.json({'error':'forbidden_or_unknown_endpoint','path':path},403)
|
||||
except Exception as e:
|
||||
audit(actor,'api_error',path,{'error':str(e),'data':data}); self.json({'error':str(e)},500)
|
||||
def main():
|
||||
init_db(); print(f'AIOS WebOps listening on {HOST}:{PORT} for {WEBOPS_DOMAIN}', flush=True); ThreadingHTTPServer((HOST,PORT),H).serve_forever()
|
||||
if __name__=='__main__': main()
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, display_name TEXT, role TEXT NOT NULL CHECK(role IN ('owner','admin','user')), status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','disabled')), password_hash TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS node_status(node_id TEXT PRIMARY KEY CHECK(node_id IN ('server-hw','server-local')), health TEXT NOT NULL DEFAULT 'unknown', active INTEGER NOT NULL DEFAULT 0, last_seen_at TEXT, score INTEGER NOT NULL DEFAULT 0, note TEXT);
|
||||
CREATE TABLE IF NOT EXISTS routing_policy(role TEXT PRIMARY KEY CHECK(role IN ('owner','admin','user')), primary_node TEXT NOT NULL CHECK(primary_node IN ('server-hw','server-local')), backup_node TEXT NOT NULL CHECK(backup_node IN ('server-hw','server-local')), active_node TEXT NOT NULL CHECK(active_node IN ('server-hw','server-local')), failover_priority INTEGER NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS approvals(id TEXT PRIMARY KEY, action TEXT NOT NULL, payload TEXT NOT NULL, requested_by TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','executed','failed')), risk TEXT NOT NULL DEFAULT 'medium', created_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT, result TEXT);
|
||||
CREATE TABLE IF NOT EXISTS operations(id TEXT PRIMARY KEY, op_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','running','succeeded','failed','skipped')), created_by TEXT NOT NULL, created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT, result TEXT);
|
||||
CREATE TABLE IF NOT EXISTS audit_logs(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, target TEXT, payload TEXT, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS events(id TEXT PRIMARY KEY, channel TEXT NOT NULL, actor TEXT, event_type TEXT NOT NULL, payload TEXT NOT NULL, route_target TEXT, status TEXT NOT NULL DEFAULT 'received', created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS quota_policy(username TEXT PRIMARY KEY, daily_limit INTEGER NOT NULL DEFAULT 100, used_today INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS model_policy(role TEXT PRIMARY KEY CHECK(role IN ('owner','admin','user')), default_model TEXT NOT NULL, allowed_models TEXT NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS memory_items(id TEXT PRIMARY KEY, username TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'user', content TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS owner_notifications(id TEXT PRIMARY KEY, event_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', created_at TEXT NOT NULL, sent_at TEXT);
|
||||
CREATE TABLE IF NOT EXISTS self_optimization_suggestions(id TEXT PRIMARY KEY, category TEXT NOT NULL, severity TEXT NOT NULL, title TEXT NOT NULL, detail TEXT NOT NULL, proposed_fix TEXT, safe_auto_fix INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'open', created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS uat_results(id TEXT PRIMARY KEY, run_id TEXT NOT NULL, tc_id TEXT NOT NULL, category TEXT NOT NULL, status TEXT NOT NULL, latency_ms INTEGER, detail TEXT, created_at TEXT NOT NULL);
|
||||
@@ -0,0 +1,5 @@
|
||||
INSERT OR IGNORE INTO node_status(node_id, health, active, score, last_seen_at, note) VALUES ('server-hw','ok',1,100,datetime('now'),'primary logical node'),('server-local','ok',0,95,datetime('now'),'backup logical node');
|
||||
INSERT OR IGNORE INTO routing_policy(role, primary_node, backup_node, active_node, failover_priority, updated_at) VALUES ('owner','server-hw','server-local','server-hw',1,datetime('now')),('admin','server-hw','server-local','server-hw',2,datetime('now')),('user','server-hw','server-local','server-hw',3,datetime('now'));
|
||||
INSERT OR IGNORE INTO users(id, username, display_name, role, status, password_hash, created_at, updated_at) VALUES ('bootstrap-owner','owner','Bootstrap Owner','owner','approved','bootstrap-change-me',datetime('now'),datetime('now')),('bootstrap-admin','admin','Bootstrap Admin','admin','approved','bootstrap-change-me',datetime('now'),datetime('now')),('sample-user','sample-user','Sample User','user','approved','bootstrap-change-me',datetime('now'),datetime('now'));
|
||||
INSERT OR IGNORE INTO model_policy(role, default_model, allowed_models, updated_at) VALUES ('owner','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b","gpt-5.5"]',datetime('now')),('admin','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b"]',datetime('now')),('user','deepseekv4-flash','["deepseekv4-flash"]',datetime('now'));
|
||||
INSERT OR IGNORE INTO quota_policy(username, daily_limit, used_today, updated_at) VALUES ('owner',10000,0,datetime('now')),('admin',2000,0,datetime('now')),('sample-user',200,0,datetime('now'));
|
||||
@@ -0,0 +1,43 @@
|
||||
CREATE TABLE IF NOT EXISTS life_signals(
|
||||
id TEXT PRIMARY KEY,
|
||||
category TEXT NOT NULL,
|
||||
severity TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
detail TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS life_plans(
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
detail TEXT NOT NULL,
|
||||
linked_signal_ids TEXT NOT NULL,
|
||||
proposed_actions TEXT NOT NULL,
|
||||
risk TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
created_by TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS patch_queue(
|
||||
id TEXT PRIMARY KEY,
|
||||
patch_type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
target_area TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
submitted_by TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
risk TEXT NOT NULL DEFAULT 'medium',
|
||||
created_at TEXT NOT NULL,
|
||||
applied_at TEXT,
|
||||
result TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS baseline_versions(
|
||||
id TEXT PRIMARY KEY,
|
||||
version_name TEXT NOT NULL,
|
||||
package_name TEXT,
|
||||
summary TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
created_by TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS gitops_baseline(
|
||||
id TEXT PRIMARY KEY,
|
||||
active_version TEXT NOT NULL,
|
||||
active_tag TEXT NOT NULL,
|
||||
source_package TEXT,
|
||||
entry TEXT NOT NULL,
|
||||
update_mode TEXT NOT NULL DEFAULT 'git_patch_only',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS gitops_versions(version TEXT PRIMARY KEY, tag TEXT NOT NULL, level TEXT NOT NULL CHECK(level IN ('V','R','C','baseline')), summary TEXT, status TEXT NOT NULL DEFAULT 'draft', created_by TEXT NOT NULL DEFAULT 'system', created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS gitops_patches(id TEXT PRIMARY KEY, patch_name TEXT NOT NULL, branch TEXT NOT NULL, base_ref TEXT NOT NULL DEFAULT 'main', target_version TEXT, risk TEXT NOT NULL DEFAULT 'medium', status TEXT NOT NULL DEFAULT 'draft', created_by TEXT NOT NULL, created_at TEXT NOT NULL, applied_at TEXT, result TEXT);
|
||||
CREATE TABLE IF NOT EXISTS llm_providers(id TEXT PRIMARY KEY, owner_username TEXT NOT NULL, name TEXT NOT NULL, provider_type TEXT NOT NULL DEFAULT 'openai_compatible', base_url TEXT NOT NULL, auth_mode TEXT NOT NULL DEFAULT 'bearer', api_key_secret_ref TEXT, scope TEXT NOT NULL DEFAULT 'user' CHECK(scope IN ('global','role','user')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS llm_models(id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, owner_username TEXT NOT NULL, model_name TEXT NOT NULL, display_name TEXT, context_window INTEGER DEFAULT 0, input_modalities TEXT NOT NULL DEFAULT '["text"]', output_modalities TEXT NOT NULL DEFAULT '["text"]', supports_tools INTEGER NOT NULL DEFAULT 0, supports_vision INTEGER NOT NULL DEFAULT 0, supports_reasoning INTEGER NOT NULL DEFAULT 0, cost_tier TEXT NOT NULL DEFAULT 'custom', is_base INTEGER NOT NULL DEFAULT 0, scope TEXT NOT NULL DEFAULT 'user' CHECK(scope IN ('global','role','user')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(owner_username, provider_id, model_name));
|
||||
CREATE TABLE IF NOT EXISTS user_model_priority(username TEXT NOT NULL, model_id TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 100, enabled INTEGER NOT NULL DEFAULT 1, fallback_policy TEXT NOT NULL DEFAULT 'try_next_on_error', updated_at TEXT NOT NULL, PRIMARY KEY(username, model_id));
|
||||
CREATE TABLE IF NOT EXISTS model_route_decisions(id TEXT PRIMARY KEY, username TEXT NOT NULL, task_type TEXT NOT NULL, selected_model_id TEXT, candidate_order TEXT NOT NULL, reason TEXT NOT NULL, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS user_runtime_profiles(username TEXT PRIMARY KEY, role TEXT NOT NULL DEFAULT 'user', tier TEXT NOT NULL DEFAULT 'regular', runtime_mode TEXT NOT NULL DEFAULT 'shared', allow_dedicated INTEGER NOT NULL DEFAULT 0, allow_custom_llm INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS runtime_instances(id TEXT PRIMARY KEY, username TEXT NOT NULL, runtime_type TEXT NOT NULL, backend TEXT NOT NULL DEFAULT 'hermes', status TEXT NOT NULL DEFAULT 'registered', endpoint TEXT, last_seen_at TEXT, created_at TEXT NOT NULL, metadata TEXT NOT NULL DEFAULT '{}');
|
||||
CREATE TABLE IF NOT EXISTS model_audit_logs(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, target TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL);
|
||||
INSERT OR IGNORE INTO gitops_baseline(id,active_version,active_tag,source_package,entry,update_mode,status,created_at,updated_at) VALUES('current','V1R1C0','V1R1C0','AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706','https://cnnetmkt.top','git_patch_only','active',datetime('now'),datetime('now'));
|
||||
INSERT OR IGNORE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES('V1R1C0','V1R1C0','R','Add MultiUser runtime selector, GitOps online baseline and user custom LLM priority registry.','ready','system',datetime('now'));
|
||||
Executable
+244
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import json, os, re, sqlite3, subprocess, time, uuid, hashlib, urllib.parse, html
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(os.getenv('AIOS_GITOPS_ROOT', '/data/aios-gitops'))
|
||||
WORKSPACE = Path(os.getenv('AIOS_GITOPS_WORKSPACE', str(ROOT / 'workspace')))
|
||||
DB_PATH = Path(os.getenv('AIOS_GITOPS_DB', str(ROOT / 'runtime' / 'gitops_model.sqlite3')))
|
||||
SECRETS_DIR = Path(os.getenv('AIOS_GITOPS_SECRETS_DIR', str(ROOT / 'runtime' / 'secrets')))
|
||||
TOKEN_FILE = Path(os.getenv('AIOS_GITOPS_TOKEN_FILE', str(SECRETS_DIR / 'gitops.tokens')))
|
||||
HOST = os.getenv('AIOS_GITOPS_HOST', '127.0.0.1')
|
||||
PORT = int(os.getenv('AIOS_GITOPS_PORT', '18890'))
|
||||
ENTRY = os.getenv('AIOS_ENTRY', 'https://cnnetmkt.top')
|
||||
DEV_ALLOW = os.getenv('AIOS_GITOPS_DEV_ALLOW', '0') == '1'
|
||||
BASE_MODELS = [
|
||||
{'id':'base.deepseekv4-flash','provider':'one-api','model':'deepseekv4-flash','display':'DeepSeek V4 Flash','tier':'regular','reasoning':1,'vision':0,'tools':1,'context':128000},
|
||||
{'id':'base.qwen3.6-35b','provider':'one-api','model':'qwen3.6-35b','display':'Qwen 3.6 35B','tier':'pro','reasoning':1,'vision':0,'tools':1,'context':262144},
|
||||
{'id':'base.gpt-5.5','provider':'openai','model':'gpt-5.5','display':'GPT-5.5','tier':'ultra','reasoning':1,'vision':1,'tools':1,'context':256000},
|
||||
]
|
||||
|
||||
def now() -> str: return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
def uid(prefix='id') -> str: return f'{prefix}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
|
||||
def esc(x: Any) -> str: return html.escape('' if x is None else str(x))
|
||||
def safe_name(s: str) -> str: return re.sub(r'[^a-zA-Z0-9_.@-]+', '_', s or 'unknown')[:120]
|
||||
def db():
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True); con = sqlite3.connect(DB_PATH); con.row_factory = sqlite3.Row; return con
|
||||
def rows(sql, args=()):
|
||||
with db() as con: return [dict(r) for r in con.execute(sql, args).fetchall()]
|
||||
def one(sql, args=()):
|
||||
r = rows(sql, args); return r[0] if r else None
|
||||
def exec_sql(sql, args=()):
|
||||
with db() as con: con.execute(sql, args); con.commit()
|
||||
def audit(actor, action, target='', payload=None): exec_sql('INSERT INTO model_audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'), actor, action, target, json.dumps(payload or {}, ensure_ascii=False), now()))
|
||||
|
||||
def init_db():
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
schema = Path(__file__).resolve().parents[1] / 'migrations' / '004_gitops_custom_llm_runtime.sql'
|
||||
with db() as con:
|
||||
if schema.exists(): con.executescript(schema.read_text(encoding='utf-8'))
|
||||
else:
|
||||
con.executescript('''CREATE TABLE IF NOT EXISTS gitops_baseline(id TEXT PRIMARY KEY,active_version TEXT,active_tag TEXT,source_package TEXT,entry TEXT,update_mode TEXT,status TEXT,created_at TEXT,updated_at TEXT);CREATE TABLE IF NOT EXISTS gitops_versions(version TEXT PRIMARY KEY,tag TEXT,level TEXT,summary TEXT,status TEXT,created_by TEXT,created_at TEXT);CREATE TABLE IF NOT EXISTS gitops_patches(id TEXT PRIMARY KEY,patch_name TEXT,branch TEXT,base_ref TEXT,target_version TEXT,risk TEXT,status TEXT,created_by TEXT,created_at TEXT,applied_at TEXT,result TEXT);CREATE TABLE IF NOT EXISTS llm_providers(id TEXT PRIMARY KEY,owner_username TEXT,name TEXT,provider_type TEXT,base_url TEXT,auth_mode TEXT,api_key_secret_ref TEXT,scope TEXT,status TEXT,created_at TEXT,updated_at TEXT);CREATE TABLE IF NOT EXISTS llm_models(id TEXT PRIMARY KEY,provider_id TEXT,owner_username TEXT,model_name TEXT,display_name TEXT,context_window INTEGER,input_modalities TEXT,output_modalities TEXT,supports_tools INTEGER,supports_vision INTEGER,supports_reasoning INTEGER,cost_tier TEXT,is_base INTEGER,scope TEXT,status TEXT,created_at TEXT,updated_at TEXT, UNIQUE(owner_username, provider_id, model_name));CREATE TABLE IF NOT EXISTS user_model_priority(username TEXT,model_id TEXT,priority INTEGER,enabled INTEGER,fallback_policy TEXT,updated_at TEXT,PRIMARY KEY(username,model_id));CREATE TABLE IF NOT EXISTS model_route_decisions(id TEXT PRIMARY KEY,username TEXT,task_type TEXT,selected_model_id TEXT,candidate_order TEXT,reason TEXT,created_at TEXT);CREATE TABLE IF NOT EXISTS user_runtime_profiles(username TEXT PRIMARY KEY,role TEXT,tier TEXT,runtime_mode TEXT,allow_dedicated INTEGER,allow_custom_llm INTEGER,updated_at TEXT);CREATE TABLE IF NOT EXISTS runtime_instances(id TEXT PRIMARY KEY,username TEXT,runtime_type TEXT,backend TEXT,status TEXT,endpoint TEXT,last_seen_at TEXT,created_at TEXT,metadata TEXT);CREATE TABLE IF NOT EXISTS model_audit_logs(id TEXT PRIMARY KEY,actor TEXT,action TEXT,target TEXT,payload TEXT,created_at TEXT);''')
|
||||
con.commit()
|
||||
seed_base_models()
|
||||
|
||||
def seed_base_models():
|
||||
exec_sql("INSERT OR IGNORE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ('provider.one-api','system','AIOS One API','openai_compatible','http://127.0.0.1:13000/v1','bearer','secret://one-api/global','global','active',now(),now()))
|
||||
exec_sql("INSERT OR IGNORE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", ('provider.openai','system','OpenAI Official','openai_compatible','https://api.openai.com/v1','bearer','secret://openai/global','global','active',now(),now()))
|
||||
for m in BASE_MODELS:
|
||||
pid = 'provider.openai' if m['provider']=='openai' else 'provider.one-api'
|
||||
exec_sql('''INSERT OR IGNORE INTO llm_models(id,provider_id,owner_username,model_name,display_name,context_window,input_modalities,output_modalities,supports_tools,supports_vision,supports_reasoning,cost_tier,is_base,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', (m['id'], pid, 'system', m['model'], m['display'], m['context'], '["text"]', '["text"]', m['tools'], m['vision'], m['reasoning'], m['tier'], 1, 'global', 'active', now(), now()))
|
||||
if not one('SELECT * FROM gitops_baseline WHERE id=?', ('current',)):
|
||||
exec_sql('INSERT INTO gitops_baseline(id,active_version,active_tag,source_package,entry,update_mode,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)', ('current','V1R1C0','V1R1C0','AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706',ENTRY,'git_patch_only','active',now(),now()))
|
||||
if not one('SELECT * FROM gitops_versions WHERE version=?', ('V1R1C0',)):
|
||||
exec_sql('INSERT INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)', ('V1R1C0','V1R1C0','R','MultiUser runtime selector + GitOps + custom LLM priority registry','ready','system',now()))
|
||||
|
||||
def valid_token(token: str) -> bool:
|
||||
if DEV_ALLOW: return True
|
||||
if not token or not TOKEN_FILE.exists(): return False
|
||||
return token.strip() in [x.strip() for x in TOKEN_FILE.read_text().splitlines() if x.strip()]
|
||||
def git_cmd(args, check=False):
|
||||
WORKSPACE.mkdir(parents=True, exist_ok=True); p = subprocess.run(['git'] + args, cwd=str(WORKSPACE), text=True, capture_output=True)
|
||||
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git command failed')
|
||||
return {'code': p.returncode, 'stdout': p.stdout.strip(), 'stderr': p.stderr.strip(), 'cmd': ['git']+args}
|
||||
def model_visible_to(username: str): return rows('''SELECT m.*, p.name provider_name, p.base_url, p.provider_type, p.scope provider_scope FROM llm_models m JOIN llm_providers p ON p.id=m.provider_id WHERE m.status='active' AND (m.scope='global' OR m.owner_username=? OR p.owner_username=?) ORDER BY m.is_base DESC, m.created_at ASC''', (username, username))
|
||||
def priority_for(username: str): return rows('''SELECT pr.username, pr.model_id, pr.priority, pr.enabled, pr.fallback_policy, pr.updated_at, m.model_name, m.display_name, m.is_base, m.owner_username, p.base_url, p.name provider_name FROM user_model_priority pr JOIN llm_models m ON m.id=pr.model_id JOIN llm_providers p ON p.id=m.provider_id WHERE pr.username=? AND m.status='active' ORDER BY pr.priority ASC''', (username,))
|
||||
def route_model(username: str, task_type='chat'):
|
||||
ordered = [r for r in priority_for(username) if int(r.get('enabled') or 0) == 1]
|
||||
if not ordered:
|
||||
ordered = [m for m in model_visible_to(username) if int(m.get('is_base') or 0) == 1]
|
||||
selected = dict(ordered[0]) if ordered else None
|
||||
if selected and 'id' not in selected and selected.get('model_id'):
|
||||
selected['id'] = selected.get('model_id')
|
||||
reason = 'user_priority' if priority_for(username) else 'role_default_base_model'
|
||||
candidate_ids = [(x.get('id') or x.get('model_id')) for x in ordered]
|
||||
selected_id = selected.get('id') if selected else None
|
||||
exec_sql('INSERT INTO model_route_decisions(id,username,task_type,selected_model_id,candidate_order,reason,created_at) VALUES(?,?,?,?,?,?,?)',(uid('route'), username, task_type, selected_id, json.dumps(candidate_ids, ensure_ascii=False), reason, now()))
|
||||
return {'username': username, 'task_type': task_type, 'selected': selected, 'candidate_order': ordered, 'reason': reason}
|
||||
def parse_body(handler):
|
||||
n = int(handler.headers.get('Content-Length','0') or '0')
|
||||
if n <= 0: return {}
|
||||
raw = handler.rfile.read(n).decode('utf-8')
|
||||
try: return json.loads(raw or '{}')
|
||||
except Exception: return {'_raw': raw}
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = 'AIOSGitOpsModelAPI/V1R1C0'
|
||||
def actor(self):
|
||||
auth = self.headers.get('Authorization',''); token = auth.replace('Bearer ', '').strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
|
||||
ok = valid_token(token); user = self.headers.get('X-AIOS-User') or ('dev-owner' if DEV_ALLOW else 'anonymous'); role = self.headers.get('X-AIOS-Role') or ('owner' if DEV_ALLOW else 'user')
|
||||
return {'username': safe_name(user), 'role': role, 'token_ok': ok}
|
||||
def require_write(self, roles=None):
|
||||
a = self.actor()
|
||||
if not a['token_ok']: self.send_json({'error':'unauthorized','hint':'write APIs require Bearer token or AIOS_GITOPS_DEV_ALLOW=1'}, 401); return None
|
||||
if roles and a['role'] not in roles: self.send_json({'error':'forbidden','required_roles':roles,'role':a['role']}, 403); return None
|
||||
return a
|
||||
def send_json(self, data, code=200):
|
||||
b = json.dumps(data, ensure_ascii=False, indent=2).encode('utf-8'); self.send_response(code); self.send_header('Content-Type','application/json; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
|
||||
def send_html(self, title, body, code=200):
|
||||
links = '<a href="/base">base</a> <a href="/ver">ver</a> <a href="/diff">diff</a> <a href="/patch">patch</a> <a href="/run">run</a> <a href="/model">model</a> <a href="/api">api</a>'
|
||||
html_doc = f'''<!doctype html><html><head><meta charset="utf-8"><title>{esc(title)}</title><style>body{{font-family:Arial,"Microsoft YaHei",sans-serif;margin:24px;background:#f8fafc}}a{{margin-right:12px}}.card{{background:white;border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin:12px 0}}table{{border-collapse:collapse;width:100%;background:white}}td,th{{border:1px solid #ddd;padding:6px;text-align:left;vertical-align:top}}pre{{background:#0f172a;color:#e2e8f0;padding:12px;border-radius:8px;overflow:auto}}</style></head><body><h1>{esc(title)}</h1><div class="card">{links}</div>{body}</body></html>'''
|
||||
b = html_doc.encode('utf-8'); self.send_response(code); self.send_header('Content-Type','text/html; charset=utf-8'); self.send_header('Content-Length',str(len(b))); self.end_headers(); self.wfile.write(b)
|
||||
def table(self, data):
|
||||
if not data: return '<p>No records.</p>'
|
||||
cols = list(data[0].keys()); return '<table><thead><tr>'+''.join(f'<th>{esc(c)}</th>' for c in cols)+'</tr></thead><tbody>'+''.join('<tr>'+''.join(f'<td>{esc(r.get(c))}</td>' for c in cols)+'</tr>' for r in data)+'</tbody></table>'
|
||||
def do_GET(self):
|
||||
init_db(); u = urllib.parse.urlparse(self.path); path = u.path; q = urllib.parse.parse_qs(u.query); username = self.actor()['username']
|
||||
try:
|
||||
if path in ('/health','/api/health'): self.send_json({'status':'ok','version':'V1R1C0','entry':ENTRY,'workspace':str(WORKSPACE),'db':str(DB_PATH)}); return
|
||||
if path in ('/api/status','/api'):
|
||||
if path == '/api': self.send_html('AIOS V1R1C0 API', '<pre>'+esc(json.dumps(api_spec(),ensure_ascii=False,indent=2))+'</pre>'); return
|
||||
self.send_json({'version':'V1R1C0','git':git_cmd(['status','--short']),'baseline':one('SELECT * FROM gitops_baseline WHERE id=?',('current',))}); return
|
||||
if path in ('/api/base','/base'):
|
||||
data = one('SELECT * FROM gitops_baseline WHERE id=?',('current',)) or {}; self.send_html('Baseline', '<pre>'+esc(json.dumps(data,ensure_ascii=False,indent=2))+'</pre>') if path == '/base' else self.send_json(data); return
|
||||
if path in ('/api/ver','/ver'):
|
||||
data = rows('SELECT * FROM gitops_versions ORDER BY created_at DESC'); self.send_html('Versions', self.table(data)) if path == '/ver' else self.send_json(data); return
|
||||
if path in ('/api/patch','/patch'):
|
||||
data = rows('SELECT * FROM gitops_patches ORDER BY created_at DESC'); self.send_html('Patches', self.table(data)) if path == '/patch' else self.send_json(data); return
|
||||
if path in ('/api/diff','/diff'):
|
||||
frm = (q.get('from') or q.get('from_') or ['HEAD~1'])[0]; to = (q.get('to') or ['HEAD'])[0]; diff = git_cmd(['diff', frm, to])
|
||||
self.send_html('Diff', f'<p>from={esc(frm)} to={esc(to)}</p><pre>{esc(diff["stdout"] or diff["stderr"])}</pre>') if path == '/diff' else self.send_json({'from':frm,'to':to,'diff':diff}); return
|
||||
if path in ('/api/model/base','/api/model/custom','/api/model/priority','/model'):
|
||||
if path == '/api/model/base': self.send_json([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==1]); return
|
||||
if path == '/api/model/custom': self.send_json([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==0]); return
|
||||
if path == '/api/model/priority': self.send_json(priority_for(username)); return
|
||||
body = '<h2>Base Models</h2>'+self.table([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==1]); body += '<h2>Custom Models</h2>'+self.table([m for m in model_visible_to(username) if int(m.get('is_base') or 0)==0]); body += '<h2>Priority</h2>'+self.table(priority_for(username)); self.send_html('Model Registry', body); return
|
||||
if path in ('/api/runtime/status','/run'):
|
||||
data = rows('SELECT * FROM runtime_instances ORDER BY created_at DESC LIMIT 100'); self.send_html('Runtime', self.table(data)) if path == '/run' else self.send_json(data); return
|
||||
if path == '/api/audit': self.send_json(rows('SELECT * FROM model_audit_logs ORDER BY created_at DESC LIMIT 200')); return
|
||||
self.send_json({'error':'not_found','path':path},404)
|
||||
except Exception as e: self.send_json({'error':str(e)},500)
|
||||
def do_POST(self):
|
||||
init_db(); path = urllib.parse.urlparse(self.path).path; data = parse_body(self)
|
||||
try:
|
||||
if path == '/api/model/custom/add':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
username = safe_name(data.get('username') or a['username']); provider_name = data.get('provider_name') or data.get('name') or 'custom-provider'; base_url = (data.get('base_url') or '').rstrip('/'); model_name = data.get('model_name') or data.get('model')
|
||||
if not base_url or not model_name: self.send_json({'error':'base_url and model_name required'},400); return
|
||||
secret_ref = data.get('api_key_secret_ref')
|
||||
if data.get('api_key'):
|
||||
sdir = SECRETS_DIR / 'llm' / safe_name(username); sdir.mkdir(parents=True, exist_ok=True); secret_id = uid('llmkey'); sp = sdir / f'{secret_id}.secret'; sp.write_text(str(data['api_key']), encoding='utf-8'); os.chmod(sp, 0o600); secret_ref = f'secret://llm/{safe_name(username)}/{secret_id}'
|
||||
provider_id = data.get('provider_id') or f'provider.custom.{safe_name(username)}.{hashlib.sha1((provider_name+base_url).encode()).hexdigest()[:8]}'; model_id = data.get('model_id') or f'custom.{safe_name(username)}.{hashlib.sha1((provider_id+model_name).encode()).hexdigest()[:10]}'
|
||||
exec_sql('''INSERT OR REPLACE INTO llm_providers(id,owner_username,name,provider_type,base_url,auth_mode,api_key_secret_ref,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)''',(provider_id, username, provider_name, data.get('provider_type','openai_compatible'), base_url, data.get('auth_mode','bearer'), secret_ref, data.get('scope','user'), 'active', now(), now()))
|
||||
exec_sql('''INSERT OR REPLACE INTO llm_models(id,provider_id,owner_username,model_name,display_name,context_window,input_modalities,output_modalities,supports_tools,supports_vision,supports_reasoning,cost_tier,is_base,scope,status,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',(model_id, provider_id, username, model_name, data.get('display_name') or model_name, int(data.get('context_window') or 0), json.dumps(data.get('input_modalities') or ['text'],ensure_ascii=False), json.dumps(data.get('output_modalities') or ['text'],ensure_ascii=False), int(bool(data.get('supports_tools',1))), int(bool(data.get('supports_vision',0))), int(bool(data.get('supports_reasoning',0))), data.get('cost_tier','custom'), 0, data.get('scope','user'), 'active', now(), now()))
|
||||
if 'priority' in data: exec_sql('INSERT OR REPLACE INTO user_model_priority(username,model_id,priority,enabled,fallback_policy,updated_at) VALUES(?,?,?,?,?,?)',(username, model_id, int(data.get('priority')), int(bool(data.get('enabled',1))), data.get('fallback_policy','try_next_on_error'), now()))
|
||||
audit(a['username'],'custom_model_add',model_id,{'username':username,'provider_id':provider_id,'model_name':model_name}); self.send_json({'status':'ok','provider_id':provider_id,'model_id':model_id,'secret_ref':secret_ref}); return
|
||||
if path == '/api/model/custom/update':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
model_id = data.get('model_id'); m = one('SELECT * FROM llm_models WHERE id=?',(model_id,))
|
||||
if not m: self.send_json({'error':'model not found'},404); return
|
||||
if m['owner_username'] != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
|
||||
fields=[]; args=[]
|
||||
for col in ['display_name','status','cost_tier']:
|
||||
if col in data: fields.append(f'{col}=?'); args.append(data[col])
|
||||
for col in ['context_window','supports_tools','supports_vision','supports_reasoning']:
|
||||
if col in data: fields.append(f'{col}=?'); args.append(int(bool(data[col])) if col.startswith('supports') else int(data[col]))
|
||||
fields.append('updated_at=?'); args.append(now()); args.append(model_id); exec_sql(f'UPDATE llm_models SET {", ".join(fields)} WHERE id=?', args); audit(a['username'],'custom_model_update',model_id,data); self.send_json({'status':'updated','model_id':model_id}); return
|
||||
if path == '/api/model/delete':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
model_id = data.get('model_id'); m = one('SELECT * FROM llm_models WHERE id=?',(model_id,))
|
||||
if not m: self.send_json({'error':'model not found'},404); return
|
||||
if int(m.get('is_base') or 0)==1: self.send_json({'error':'base model cannot be deleted'},400); return
|
||||
if m['owner_username'] != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
|
||||
exec_sql('UPDATE llm_models SET status=?,updated_at=? WHERE id=?',('disabled',now(),model_id)); audit(a['username'],'custom_model_delete',model_id,{}); self.send_json({'status':'disabled','model_id':model_id}); return
|
||||
if path == '/api/model/priority/set':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
username = safe_name(data.get('username') or a['username'])
|
||||
if username != a['username'] and a['role'] not in ('owner','admin'): self.send_json({'error':'forbidden'},403); return
|
||||
items = data.get('items') or data.get('priority') or []; exec_sql('DELETE FROM user_model_priority WHERE username=?',(username,)); visible={m['id'] for m in model_visible_to(username)}
|
||||
for idx,item in enumerate(items):
|
||||
if isinstance(item,str): item={'model_id':item,'priority':(idx+1)*10,'enabled':1}
|
||||
mid=item.get('model_id') or item.get('id')
|
||||
if mid and mid in visible: exec_sql('INSERT OR REPLACE INTO user_model_priority(username,model_id,priority,enabled,fallback_policy,updated_at) VALUES(?,?,?,?,?,?)',(username, mid, int(item.get('priority',(idx+1)*10)), int(bool(item.get('enabled',1))), item.get('fallback_policy','try_next_on_error'), now()))
|
||||
audit(a['username'],'model_priority_set',username,{'items':items}); self.send_json({'status':'updated','username':username,'priority':priority_for(username)}); return
|
||||
if path == '/api/model/route/resolve': a=self.actor(); self.send_json(route_model(safe_name(data.get('username') or a['username']), data.get('task_type','chat'))); return
|
||||
if path == '/api/model/test':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
mid=data.get('model_id'); m=one('SELECT m.*,p.base_url,p.provider_type FROM llm_models m JOIN llm_providers p ON p.id=m.provider_id WHERE m.id=?',(mid,))
|
||||
if not m: self.send_json({'error':'model not found'},404); return
|
||||
self.send_json({'status':'ok' if m['base_url'] and m['model_name'] else 'failed','live_test':os.getenv('AIOS_ENABLE_MODEL_LIVE_TEST','0')=='1','model':m['model_name'],'base_url':m['base_url'],'note':'Network live call is disabled unless AIOS_ENABLE_MODEL_LIVE_TEST=1'}); return
|
||||
if path == '/api/runtime/select':
|
||||
a=self.actor(); username=safe_name(data.get('username') or a['username']); role=data.get('role') or a['role']; tier=data.get('tier') or ('ultra' if role=='owner' else 'regular'); risk=data.get('risk','normal')
|
||||
if risk in ('high_tool','shell','docker','write_prod'): runtime,backend='ephemeral_sandbox','openclaw_executor'
|
||||
elif role in ('owner','admin') or tier=='ultra': runtime,backend='dedicated','hermes'
|
||||
elif tier=='pro': runtime,backend='dedicated_or_shared','hermes'
|
||||
else: runtime,backend='shared','hermes'
|
||||
exec_sql('INSERT OR REPLACE INTO user_runtime_profiles(username,role,tier,runtime_mode,allow_dedicated,allow_custom_llm,updated_at) VALUES(?,?,?,?,?,?,?)',(username,role,tier,runtime,1 if runtime!='shared' else 0,1,now())); self.send_json({'username':username,'role':role,'tier':tier,'runtime_type':runtime,'backend':backend,'reason':'V1R1C0 selector policy'}); return
|
||||
if path == '/api/runtime/register':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
rid=data.get('id') or uid('rt'); exec_sql('INSERT OR REPLACE INTO runtime_instances(id,username,runtime_type,backend,status,endpoint,last_seen_at,created_at,metadata) VALUES(?,?,?,?,?,?,?,?,?)',(rid,safe_name(data.get('username') or 'shared'),data.get('runtime_type','shared'),data.get('backend','hermes'),data.get('status','registered'),data.get('endpoint'),now(),now(),json.dumps(data.get('metadata') or {},ensure_ascii=False))); audit(a['username'],'runtime_register',rid,data); self.send_json({'status':'registered','id':rid}); return
|
||||
if path == '/api/runtime/heartbeat': exec_sql('UPDATE runtime_instances SET status=?,last_seen_at=?,metadata=? WHERE id=?',(data.get('status','ok'),now(),json.dumps(data.get('metadata') or {},ensure_ascii=False),data.get('id'))); self.send_json({'status':'ok','id':data.get('id')}); return
|
||||
if path == '/api/branch':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
branch=data.get('branch'); base=data.get('base','main')
|
||||
if not branch: self.send_json({'error':'branch required'},400); return
|
||||
git_cmd(['fetch','--all']); git_cmd(['checkout',base]); git_cmd(['checkout','-B',branch],check=True); audit(a['username'],'git_branch',branch,data); self.send_json({'status':'ok','branch':branch}); return
|
||||
if path == '/api/file':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
branch=data.get('branch') or 'main'; rel=data.get('path'); content=data.get('content',''); msg=data.get('message') or f'chore(gitops): update {rel}'
|
||||
if not rel or rel.startswith('/') or '..' in Path(rel).parts: self.send_json({'error':'invalid path'},400); return
|
||||
git_cmd(['checkout',branch],check=True); target=WORKSPACE/rel; target.parent.mkdir(parents=True,exist_ok=True); target.write_text(content,encoding='utf-8'); git_cmd(['add',rel],check=True); commit=git_cmd(['commit','-m',msg]); audit(a['username'],'git_file_update',rel,{'branch':branch,'commit':commit}); self.send_json({'status':'committed','path':rel,'commit':commit}); return
|
||||
if path == '/api/commit':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
msg=data.get('message') or 'chore(gitops): commit changes'; git_cmd(['add','.'],check=True); out=git_cmd(['commit','-m',msg]); audit(a['username'],'git_commit','workspace',{'message':msg}); self.send_json({'status':'ok','commit':out}); return
|
||||
if path == '/api/tag':
|
||||
a = self.require_write(['owner']);
|
||||
if not a: return
|
||||
ver=data.get('version') or data.get('tag')
|
||||
if not re.match(r'^V\d+R\d+C\d+(-[a-zA-Z0-9_.-]+)?$', ver or ''): self.send_json({'error':'version must match VxRxCx'},400); return
|
||||
msg=data.get('message') or f'release {ver}'; out=git_cmd(['tag','-a',ver,'-m',msg]);
|
||||
if data.get('push',False): git_cmd(['push','origin',ver])
|
||||
level='V' if re.match(r'^V[1-9]\d*R0C0', ver) else ('R' if re.match(r'^V\d+R[1-9]\d*C0', ver) else 'C'); exec_sql('INSERT OR REPLACE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)',(ver,ver,level,msg,'ready',a['username'],now())); exec_sql('UPDATE gitops_baseline SET active_version=?,active_tag=?,updated_at=? WHERE id=?',(ver,ver,now(),'current')); audit(a['username'],'git_tag',ver,{'level':level}); self.send_json({'status':'tagged','version':ver,'git':out}); return
|
||||
if path == '/api/rollback':
|
||||
a = self.require_write(['owner','admin']);
|
||||
if not a: return
|
||||
target=data.get('target_version') or data.get('version')
|
||||
if not target: self.send_json({'error':'target_version required'},400); return
|
||||
out=git_cmd(['checkout',target]); audit(a['username'],'git_rollback',target,{}); self.send_json({'status':'rollback_checkout','target':target,'git':out}); return
|
||||
if path == '/api/nl':
|
||||
a = self.require_write();
|
||||
if not a: return
|
||||
text=data.get('text',''); intent='unknown'; plan=[]
|
||||
if '回滚' in text or 'rollback' in text.lower(): intent='rollback'; plan=['extract target VxRxCx','call /api/rollback','run smoke test']
|
||||
elif '对比' in text or 'diff' in text.lower(): intent='diff'; plan=['extract from/to refs','call /api/diff']
|
||||
elif '模型' in text and ('优先' in text or 'priority' in text.lower()): intent='set_model_priority'; plan=['resolve model ids','call /api/model/priority/set']
|
||||
elif '模型' in text and ('添加' in text or '新增' in text or 'add' in text.lower()): intent='add_custom_model'; plan=['validate provider','store api key as secret_ref','call /api/model/custom/add']
|
||||
elif 'patch' in text.lower() or '补丁' in text: intent='create_patch'; plan=['create patch branch','commit patch metadata','run tests']
|
||||
self.send_json({'intent':intent,'action_plan':plan,'execute':False,'safety_check':'V-level changes require owner approval'}); return
|
||||
self.send_json({'error':'not_found','path':path},404)
|
||||
except Exception as e: self.send_json({'error':str(e)},500)
|
||||
|
||||
def api_spec(): return {'version':'V1R1C0','short_links':['/base','/ver','/diff','/patch','/run','/model','/api'],'gitops_get':['/api/status','/api/base','/api/ver','/api/diff','/api/patch'],'gitops_post':['/api/branch','/api/file','/api/commit','/api/tag','/api/rollback','/api/nl'],'model_get':['/api/model/base','/api/model/custom','/api/model/priority'],'model_post':['/api/model/custom/add','/api/model/custom/update','/api/model/delete','/api/model/priority/set','/api/model/route/resolve','/api/model/test'],'runtime':['/api/runtime/status','/api/runtime/select','/api/runtime/register','/api/runtime/heartbeat'],'auth':'write APIs require Bearer token unless AIOS_GITOPS_DEV_ALLOW=1 for local acceptance tests'}
|
||||
def main(): init_db(); print(f'AIOS V1R1C0 GitOps Model API listening on {HOST}:{PORT}'); ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
|
||||
if __name__ == '__main__': main()
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import json, re, sys
|
||||
def parse(text: str):
|
||||
t = text.lower(); version = re.findall(r'V\d+R\d+C\d+', text)
|
||||
if '回滚' in text or 'rollback' in t: return {'intent':'rollback','target_version':version[0] if version else None,'risk':'high','requires_approval':True}
|
||||
if '对比' in text or 'diff' in t: return {'intent':'diff','versions':version,'risk':'low','requires_approval':False}
|
||||
if '添加' in text and '模型' in text: return {'intent':'add_custom_model','risk':'medium','requires_approval':False}
|
||||
if '优先级' in text or 'priority' in t: return {'intent':'set_model_priority','risk':'medium','requires_approval':False}
|
||||
if '发布' in text or 'release' in t:
|
||||
level = 'V' if version and re.match(r'V\d+R0C0', version[0]) else ('R' if version and re.match(r'V\d+R\d+C0', version[0]) else 'C')
|
||||
return {'intent':'release','version':version[0] if version else None,'change_level':level,'risk':'critical' if level=='V' else 'medium'}
|
||||
return {'intent':'unknown','risk':'unknown','requires_approval':True}
|
||||
if __name__ == '__main__': print(json.dumps(parse(' '.join(sys.argv[1:])), ensure_ascii=False, indent=2))
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import json, os, sys, urllib.request
|
||||
API = os.getenv('AIOS_GITOPS_API', 'http://127.0.0.1:18890/api')
|
||||
TOKEN = os.getenv('AIOS_GITOPS_TOKEN', '')
|
||||
def call(path, payload=None, method=None):
|
||||
url = API + path; data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode('utf-8')
|
||||
req = urllib.request.Request(url, data=data, method=method or ('POST' if payload is not None else 'GET'))
|
||||
if data: req.add_header('Content-Type','application/json')
|
||||
if TOKEN: req.add_header('Authorization','Bearer '+TOKEN)
|
||||
req.add_header('X-AIOS-User', os.getenv('AIOS_ACTOR','openclaw')); req.add_header('X-AIOS-Role', os.getenv('AIOS_ROLE','admin'))
|
||||
with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode('utf-8'))
|
||||
def main(argv):
|
||||
if len(argv) < 2: print('usage: openclaw_gitops_commands.py status|diff|release|rollback|model-priority|model-add ...'); return 2
|
||||
cmd = argv[1]
|
||||
if cmd == 'status': print(json.dumps(call('/status'), ensure_ascii=False, indent=2)); return 0
|
||||
if cmd == 'diff': print(json.dumps(call('/diff?from='+argv[2]+'&to='+argv[3]), ensure_ascii=False, indent=2)); return 0
|
||||
if cmd == 'release': print(json.dumps(call('/tag', {'version':argv[2], 'message':'release '+argv[2]}), ensure_ascii=False, indent=2)); return 0
|
||||
if cmd == 'rollback': print(json.dumps(call('/rollback', {'target_version':argv[2]}), ensure_ascii=False, indent=2)); return 0
|
||||
if cmd == 'model-add': print(json.dumps(call('/model/custom/add', {'provider_name':argv[2], 'base_url':argv[3], 'model_name':argv[4], 'priority':int(argv[5]) if len(argv)>5 else 10}), ensure_ascii=False, indent=2)); return 0
|
||||
if cmd == 'model-priority': print(json.dumps(call('/model/priority/set', {'items': argv[2:]}), ensure_ascii=False, indent=2)); return 0
|
||||
print('unknown command', cmd); return 2
|
||||
if __name__ == '__main__': raise SystemExit(main(sys.argv))
|
||||
@@ -0,0 +1 @@
|
||||
<a href="/base">基线</a> <a href="/ver">版本</a> <a href="/diff">差异</a> <a href="/patch">补丁</a> <a href="/model">模型</a> <a href="/run">运行</a> <a href="/git">仓库</a>
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"architecture_version": "V1",
|
||||
"release_version": "R1",
|
||||
"change_version": "C0",
|
||||
"components": [
|
||||
"webops",
|
||||
"owner-admin-user",
|
||||
"npm",
|
||||
"casdoor",
|
||||
"openclaw",
|
||||
"hermes",
|
||||
"one-api",
|
||||
"gitops-control",
|
||||
"baseline-registry",
|
||||
"custom-llm-registry",
|
||||
"runtime-selector"
|
||||
],
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"proxy": "npm",
|
||||
"runtime_sync": "openclaw"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"active_version": "V1R1C0",
|
||||
"active_tag": "V1R1C0-baseline-20260706",
|
||||
"mode": "gitops-online-baseline",
|
||||
"version_policy": "VxRxCx",
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_deprecated": true,
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"links": {
|
||||
"git": "/git",
|
||||
"baseline": "/base",
|
||||
"versions": "/ver",
|
||||
"diff": "/diff",
|
||||
"patch": "/patch",
|
||||
"model": "/model",
|
||||
"runtime": "/run",
|
||||
"api": "/api",
|
||||
"hook": "/hook"
|
||||
},
|
||||
"features": {
|
||||
"multiuser_runtime": true,
|
||||
"shared_dedicated_runtime": true,
|
||||
"custom_llm": true,
|
||||
"user_model_priority": true,
|
||||
"chatgpt_rw": true,
|
||||
"codex_rw": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
digital_life:
|
||||
name: AIOS v23 v5 Digital Life Core
|
||||
entry: https://ops.cnnetmkt.top/life
|
||||
loop:
|
||||
- observe
|
||||
- diagnose
|
||||
- plan
|
||||
- approve
|
||||
- act
|
||||
- learn
|
||||
- rollback
|
||||
safety:
|
||||
high_risk_requires_owner_approval: true
|
||||
production_delta_only: true
|
||||
backup_before_change: true
|
||||
rollback_on_failed_uat: true
|
||||
chatgpt_can_write_package_assets: true
|
||||
codex_deploys_to_runtime: true
|
||||
@@ -0,0 +1,6 @@
|
||||
AIOS_PUBLIC_DOMAIN=https://cnnetmkt.top
|
||||
AIOS_WEBOPS_DOMAIN=https://ops.cnnetmkt.top
|
||||
AIOS_API_DOMAIN=https://api.cnnetmkt.top
|
||||
AIOS_WECHAT_CALLBACK_DOMAIN=https://wechat.cnnetmkt.top
|
||||
AIOS_LOGICAL_PRIMARY_NODE=server-hw
|
||||
AIOS_LOGICAL_BACKUP_NODE=server-local
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"proxy": "NPM",
|
||||
"short_links": {
|
||||
"/git": "Gitea or existing Git Web UI",
|
||||
"/base": "current GitOps baseline",
|
||||
"/ver": "VxRxCx version list",
|
||||
"/diff": "version diff view",
|
||||
"/patch": "patch branch registry",
|
||||
"/run": "runtime status and selector",
|
||||
"/api": "GitOps + Model API",
|
||||
"/model": "user custom LLM registry and priority UI",
|
||||
"/hook": "Git webhook entry"
|
||||
},
|
||||
"notes": "Prefer NPM custom locations; do not overwrite raw nginx config."
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"policy": {
|
||||
"allow_user_custom_llm": true,
|
||||
"api_key_storage": "secret_file_ref_only",
|
||||
"base_models_locked_by": [
|
||||
"owner",
|
||||
"admin"
|
||||
],
|
||||
"user_can_order_priority": true,
|
||||
"fallback_policy": "priority_order_then_role_default"
|
||||
},
|
||||
"base_models": [
|
||||
{
|
||||
"id": "base.deepseekv4-flash",
|
||||
"provider": "one-api",
|
||||
"model": "deepseekv4-flash",
|
||||
"tier": "regular",
|
||||
"reasoning": true,
|
||||
"vision": false
|
||||
},
|
||||
{
|
||||
"id": "base.qwen3.6-35b",
|
||||
"provider": "one-api",
|
||||
"model": "qwen3.6-35b",
|
||||
"tier": "pro",
|
||||
"reasoning": true,
|
||||
"vision": false
|
||||
},
|
||||
{
|
||||
"id": "base.gpt-5.5",
|
||||
"provider": "openai",
|
||||
"model": "gpt-5.5",
|
||||
"tier": "ultra",
|
||||
"reasoning": true,
|
||||
"vision": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"domain": "cnnetmkt.top",
|
||||
"locations": {
|
||||
"/git": "http://127.0.0.1:3000",
|
||||
"/api": "http://127.0.0.1:18890/api",
|
||||
"/base": "http://127.0.0.1:18890/base",
|
||||
"/ver": "http://127.0.0.1:18890/ver",
|
||||
"/diff": "http://127.0.0.1:18890/diff",
|
||||
"/patch": "http://127.0.0.1:18890/patch",
|
||||
"/run": "http://127.0.0.1:18890/run",
|
||||
"/model": "http://127.0.0.1:18890/model",
|
||||
"/hook": "http://127.0.0.1:18890/hook"
|
||||
},
|
||||
"ssl": {
|
||||
"force_ssl": true,
|
||||
"http2": true,
|
||||
"websocket_support": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
roles:
|
||||
owner:
|
||||
priority: 1
|
||||
description: 全局所有者,审批终点,可执行全部高危操作。
|
||||
admin:
|
||||
priority: 2
|
||||
description: 日常管理员,可执行日常运维,高危操作进入 owner 审批。
|
||||
user:
|
||||
priority: 3
|
||||
description: 普通用户,只能使用 AI 服务并查看自身信息。
|
||||
legacy_role_mapping:
|
||||
ultra: owner
|
||||
pro: admin
|
||||
regular: user
|
||||
@@ -0,0 +1,22 @@
|
||||
runtime:
|
||||
role_model: owner_admin_user_only
|
||||
logical_nodes:
|
||||
primary: server-hw
|
||||
backup: server-local
|
||||
domains:
|
||||
public_user: https://cnnetmkt.top
|
||||
webops: https://ops.cnnetmkt.top
|
||||
api: https://api.cnnetmkt.top
|
||||
wechat_callback: https://wechat.cnnetmkt.top
|
||||
one_person_ops:
|
||||
avoid_new_components: true
|
||||
production_core: reuse_current_deployment
|
||||
webops_overlay_storage: sqlite
|
||||
high_risk_actions_require_owner_approval:
|
||||
- failover_execute
|
||||
- failback_execute
|
||||
- routing_change
|
||||
- memory_delete
|
||||
- model_policy_change
|
||||
- rollback_release
|
||||
- apply_self_optimization_fix
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"rules": [
|
||||
{
|
||||
"match": {
|
||||
"role": "owner"
|
||||
},
|
||||
"runtime": "dedicated",
|
||||
"backend": "hermes"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"role": "admin"
|
||||
},
|
||||
"runtime": "dedicated",
|
||||
"backend": "hermes"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"tier": "ultra"
|
||||
},
|
||||
"runtime": "dedicated",
|
||||
"backend": "hermes"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"tier": "pro"
|
||||
},
|
||||
"runtime": "dedicated_or_shared",
|
||||
"backend": "hermes"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"risk": "high_tool"
|
||||
},
|
||||
"runtime": "ephemeral_sandbox",
|
||||
"backend": "openclaw_executor"
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"default": true
|
||||
},
|
||||
"runtime": "shared",
|
||||
"backend": "hermes"
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"docker_sock_direct_mount": false,
|
||||
"executor": "openclaw_or_restricted_docker_proxy"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
uat_cases:
|
||||
- WX-TEXT-001
|
||||
- WX-TEXT-002
|
||||
- WX-IMAGE-001
|
||||
- WX-FILE-001
|
||||
- WEB-REG-001
|
||||
- WEB-LOGIN-001
|
||||
- ADMIN-USERS-001
|
||||
- ADMIN-QUOTA-001
|
||||
- OWNER-APPROVAL-001
|
||||
- OWNER-FAILOVER-001
|
||||
- HA-001
|
||||
- OPT-001
|
||||
@@ -0,0 +1,22 @@
|
||||
webops_pages:
|
||||
- /dashboard
|
||||
- /nodes
|
||||
- /roles
|
||||
- /routing
|
||||
- /failover
|
||||
- /approvals
|
||||
- /operations
|
||||
- /users
|
||||
- /quota
|
||||
- /models
|
||||
- /memory
|
||||
- /channels/wechat
|
||||
- /life
|
||||
- /patches
|
||||
- /optimization
|
||||
- /simulation
|
||||
- /logs
|
||||
- /config
|
||||
- /uat
|
||||
- /versions
|
||||
- /evidence
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PKG_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
: "${AIOS_RUNTIME_ROOT:=$(pwd)}"
|
||||
: "${AIOS_RELEASE_ID:=webops-full-refresh-$(date +%Y%m%dT%H%M%S)}"
|
||||
export AIOS_RUNTIME_ROOT AIOS_RELEASE_ID
|
||||
bash "$PKG_ROOT/scripts/backup_before_fix.sh"
|
||||
python3 "$PKG_ROOT/fixes/patch_runtime_owner_admin_user.py" "$AIOS_RUNTIME_ROOT"
|
||||
bash "$PKG_ROOT/scripts/install_webops_overlay.sh" "$AIOS_RUNTIME_ROOT"
|
||||
python3 "$PKG_ROOT/scripts/apply_migrations.py" "$AIOS_RUNTIME_ROOT"
|
||||
python3 "$PKG_ROOT/scripts/validate_roles_and_domains.py" "$AIOS_RUNTIME_ROOT"
|
||||
python3 "$PKG_ROOT/scripts/validate_no_legacy_roles.py" "$AIOS_RUNTIME_ROOT" || true
|
||||
bash "$PKG_ROOT/scripts/smoke_test.sh" "$AIOS_RUNTIME_ROOT"
|
||||
python3 "$PKG_ROOT/scripts/generate_release_manifest.py" "$AIOS_RUNTIME_ROOT"
|
||||
echo "DELTA_FIX_ONLY_APPLY_OK release=$AIOS_RELEASE_ID runtime=$AIOS_RUNTIME_ROOT"
|
||||
|
||||
python3 "$PKG/fixes/patch_runtime_v5_guards.py" "$TARGET"
|
||||
echo "V5_DIGITAL_LIFE_DELTA_APPLIED"
|
||||
@@ -0,0 +1,15 @@
|
||||
# AIOS v23 全量刷新包范围说明
|
||||
|
||||
本包基于最新实际部署包生成,采用“生产只执行修正部分,全量包用于整体评估和验收”的方式。
|
||||
|
||||
## 两层交付
|
||||
|
||||
1. `delta-fix/`:生产执行层,只补齐 WebOps、角色收敛、审批、仿真、自优化、回滚入口。
|
||||
2. `source-baseline/`:最新实际部署包原样保留,用于整体评估、对比、回滚和后续版本刷新。
|
||||
|
||||
## 不做的事
|
||||
|
||||
- 不全量替换当前生产系统。
|
||||
- 不新增复杂组件栈。
|
||||
- 不让自优化系统未经审批修改高风险生产配置。
|
||||
- 不使用 `pro/ultra/regular` 作为新权限角色。
|
||||
@@ -0,0 +1,32 @@
|
||||
# AIOS v23 生产架构全量优化设计
|
||||
|
||||
## 目标形态
|
||||
|
||||
以 `https://ops.cnnetmkt.top` 为统一 WebOps 控制面,以 `https://cnnetmkt.top` 为用户入口,以 `server-hw/server-local` 为双节点执行抽象,以 `owner/admin/user` 为唯一角色体系。
|
||||
|
||||
## 核心域名
|
||||
|
||||
| 用途 | 域名 |
|
||||
|---|---|
|
||||
| 用户门户 / 注册 / 登录 | `https://cnnetmkt.top` |
|
||||
| WebOps 控制台 | `https://ops.cnnetmkt.top` |
|
||||
| API 入口 | `https://api.cnnetmkt.top` |
|
||||
| 微信回调 / 适配 | `https://wechat.cnnetmkt.top` |
|
||||
|
||||
## 角色模型
|
||||
|
||||
- `owner`:全局所有者,审批终点,能执行全部高危操作。
|
||||
- `admin`:日常管理员,可管理用户、额度、运维任务;高危动作进入 owner 审批。
|
||||
- `user`:普通用户,只能使用 AI 服务、查看自身信息。
|
||||
|
||||
历史 `ultra/pro/regular` 只能作为归档旧配置识别,不能作为新运行角色。
|
||||
|
||||
## WebOps 闭环
|
||||
|
||||
```text
|
||||
Web UI → API → 审批/任务 → 审计 → owner 微信通知 → 结果/证据 → 可回滚
|
||||
```
|
||||
|
||||
## 一人运维约束
|
||||
|
||||
默认不新增 K8s、NATS、Kafka、Redis、Qdrant、Prometheus 全家桶。新增 WebOps 是单体 overlay,不破坏当前运行系统。
|
||||
@@ -0,0 +1,67 @@
|
||||
# 01 AIOS v23 v5 数字生命体架构设计
|
||||
|
||||
## 1. 系统定位
|
||||
|
||||
AIOS v23 v5 是 Web-first 的数字生命体运行系统。它不是让系统无限自治地修改生产,而是将“感知、诊断、计划、审批、执行、学习、回滚”形成安全闭环。
|
||||
|
||||
## 2. 核心分层
|
||||
|
||||
```text
|
||||
WebOps UI
|
||||
↓
|
||||
Digital Life Core(感知/诊断/计划/建议)
|
||||
↓
|
||||
Aegis Approval Gate(owner 审批闸门)
|
||||
↓
|
||||
NEXUS Runtime Bridge(执行桥)
|
||||
↓
|
||||
AIOS Runtime / OpenClaw / Hermes / WeChat
|
||||
```
|
||||
|
||||
## 3. 数字生命体闭环
|
||||
|
||||
| 阶段 | Web 页面 | 说明 |
|
||||
|---|---|---|
|
||||
| Observe | /dashboard /logs /simulation | 采集事件、日志、用户行为、节点状态 |
|
||||
| Diagnose | /life /optimization | 发现路由异常、审批积压、节点退化、UAT 失败 |
|
||||
| Plan | /life | 生成可审查执行计划 |
|
||||
| Approve | /approvals | owner 审批高风险计划 |
|
||||
| Act | /operations /patches | OpenClaw/Codex 执行修正脚本或补丁 |
|
||||
| Learn | /versions /uat | 保存版本、验收结果和优化效果 |
|
||||
| Rollback | /operations | 失败自动或人工回滚 |
|
||||
|
||||
## 4. 一人运维约束
|
||||
|
||||
- 不引入复杂组件作为默认依赖。
|
||||
- 使用 SQLite + Python 标准库 WebOps 作为最小生产控制面。
|
||||
- OpenClaw/Codex 执行复杂修复,WebOps 负责入口、审批、审计和证据。
|
||||
- 每个高危操作必须可回滚。
|
||||
|
||||
## 5. 角色和权限
|
||||
|
||||
仅存在三类角色:owner、admin、user。
|
||||
|
||||
| 操作 | owner | admin | user |
|
||||
|---|---|---|---|
|
||||
| 审批 | 允许 | 不允许 | 不允许 |
|
||||
| 用户管理 | 允许 | 允许,不能授予 owner | 仅自己 |
|
||||
| 路由变更 | 直接执行 | 提交审批 | 不允许 |
|
||||
| 故障切换 | 审批并执行 | 提交审批 | 不允许 |
|
||||
| 补丁提交 | 允许 | 允许 | 不允许 |
|
||||
| 补丁执行 | 审批后执行 | 提交审批 | 不允许 |
|
||||
| 微信使用 | 允许 | 允许 | 允许 |
|
||||
|
||||
## 6. 节点模型
|
||||
|
||||
只使用 server-hw 和 server-local。默认 active_node 为 server-hw,backup_node 为 server-local。
|
||||
|
||||
故障迁移顺序:owner → admin → user。
|
||||
回切顺序:user → admin → owner。
|
||||
|
||||
## 7. 域名
|
||||
|
||||
- 用户门户:`https://cnnetmkt.top`
|
||||
- WebOps:`https://ops.cnnetmkt.top`
|
||||
- API:`https://api.cnnetmkt.top`
|
||||
- 微信回调:`https://wechat.cnnetmkt.top`
|
||||
- 数字生命体:`https://ops.cnnetmkt.top/life`
|
||||
@@ -0,0 +1,16 @@
|
||||
# AIOS v23 用户需求文档(owner/admin/user)
|
||||
|
||||
| 角色 | 需求 |
|
||||
|---|---|
|
||||
| owner | 通过 WebOps 看到全局状态,审批高危动作,接收个人微信告警,必要时一键回滚。 |
|
||||
| admin | 通过 WebOps 完成日常用户、额度、模型、UAT、仿真、日志处理,不能绕过 owner 审批。 |
|
||||
| user | 通过微信和 Web 正常使用 AI,支持文本、图片、文件,不看到内部错误字段。 |
|
||||
|
||||
## user 需求
|
||||
|
||||
- 微信发送普通文字,必须正常回复。
|
||||
- 微信引用普通文字,不得误入图片路线。
|
||||
- 微信发送图片,进入 Vision。
|
||||
- 微信引用 PDF/Word/Excel/TXT/CSV/ZIP 等文件,不得进入 Vision。
|
||||
- 用户 Web 注册、登录、查看自身额度和绑定状态。
|
||||
- 不得看到 `VISION_ROUTE_TARGET`、`FAIL_REASON`、Traceback 等内部字段。
|
||||
@@ -0,0 +1,37 @@
|
||||
# Codex / OpenClaw 生产部署执行要求
|
||||
|
||||
## 执行入口
|
||||
|
||||
```bash
|
||||
bash package_verify_full.sh
|
||||
bash delta-fix/apply_delta_fix_only.sh
|
||||
bash tests/run_acceptance_with_autofix.sh
|
||||
```
|
||||
|
||||
## 多 Agent 并发分工
|
||||
|
||||
| Agent | 职责 |
|
||||
|---|---|
|
||||
| agent-backup | 执行备份,记录 manifest。 |
|
||||
| agent-webops | 安装 WebOps overlay,验证页面和 API。 |
|
||||
| agent-role-policy | 验证 owner/admin/user 收敛。 |
|
||||
| agent-wechat-sim | 跑 simulated-wechat 文本/图片/文件/引用矩阵。 |
|
||||
| agent-admin-web | 跑注册、登录、用户管理、审批、额度、模型 Web 操作测试。 |
|
||||
| agent-selfopt | 对失败测试生成建议,尝试安全修复并复测。 |
|
||||
| agent-reporter | 汇总证据、生成验收报告、通知 owner 微信。 |
|
||||
|
||||
## 执行顺序
|
||||
|
||||
1. 全包校验。
|
||||
2. 当前运行系统备份。
|
||||
3. 安装 WebOps overlay。
|
||||
4. 应用 owner/admin/user 角色收敛配置。
|
||||
5. 应用数据库迁移。
|
||||
6. 启动 WebOps。
|
||||
7. smoke test。
|
||||
8. simulated-wechat 仿真。
|
||||
9. Web 注册/登录/管理测试。
|
||||
10. admin/owner 操作测试。
|
||||
11. UAT with self-optimization。
|
||||
12. 证据导出。
|
||||
13. 生成新版本 manifest。
|
||||
@@ -0,0 +1,53 @@
|
||||
# 03 Codex / OpenClaw v5 生产部署执行指令
|
||||
|
||||
## 0. 任务角色
|
||||
|
||||
Codex 是部署总控,OpenClaw 可启用多个 agent 并行执行。ChatGPT 可读写本包文档、配置和补丁内容,Codex 负责实际部署、验证和回写基线。
|
||||
|
||||
## 1. 严格约束
|
||||
|
||||
- 角色只允许 owner/admin/user。
|
||||
- 节点只允许 server-hw/server-local。
|
||||
- 生产上只执行 `delta-fix/apply_delta_fix_only.sh`,不得无差别覆盖整个运行系统。
|
||||
- 所有高危动作必须先备份,再审批,再执行,再验证。
|
||||
- 验收失败必须运行自诊断和安全自修复;仍失败则回滚。
|
||||
|
||||
## 2. 推荐多 agent 分工
|
||||
|
||||
| Agent | 任务 |
|
||||
|---|---|
|
||||
| agent-backup | 执行备份,生成备份证据 |
|
||||
| agent-webops | 部署 active-runtime WebOps 覆盖层 |
|
||||
| agent-route-guard | 执行媒体/文件/文本路由保护修正 |
|
||||
| agent-ui | 验证所有 Web UI 页面和按钮 |
|
||||
| agent-uat | 执行微信仿真、注册登录、管理员操作、owner 审批测试 |
|
||||
| agent-life-core | 执行数字生命体诊断、建议和安全自修复 |
|
||||
| agent-reporter | 汇总结果,通知 owner 微信,写入新版本基线 |
|
||||
|
||||
## 3. 执行命令
|
||||
|
||||
```bash
|
||||
bash package_verify_full.sh
|
||||
bash scripts/silent_install_v5.sh
|
||||
AIOS_RUNTIME_ROOT=<current-runtime-root> bash delta-fix/apply_delta_fix_only.sh
|
||||
bash tests/run_v5_acceptance_with_autofix.sh
|
||||
bash scripts/export_evidence.sh
|
||||
```
|
||||
|
||||
## 4. 部署后 Web 地址
|
||||
|
||||
- 用户门户:`https://cnnetmkt.top`
|
||||
- WebOps:`https://ops.cnnetmkt.top`
|
||||
- API:`https://api.cnnetmkt.top`
|
||||
- 微信回调:`https://wechat.cnnetmkt.top`
|
||||
- 数字生命体:`https://ops.cnnetmkt.top/life`
|
||||
|
||||
## 5. 成功标准
|
||||
|
||||
- WebOps 页面全部可访问。
|
||||
- owner/admin/user 权限符合矩阵。
|
||||
- user 可注册、登录、微信收发。
|
||||
- admin 可管理用户、额度和低风险运维。
|
||||
- owner 可审批高风险动作。
|
||||
- 微信文字、图片、文件引用路由正确。
|
||||
- UAT 通过;失败项有自优化建议、修复尝试和可回滚证据。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 只执行修正部分 Runbook
|
||||
|
||||
生产上只执行:
|
||||
|
||||
```bash
|
||||
bash delta-fix/apply_delta_fix_only.sh
|
||||
```
|
||||
|
||||
该脚本只做:备份、active role marker、WebOps overlay、SQLite 迁移、release manifest、smoke test。
|
||||
|
||||
不做:不覆盖历史文档、不解包替换 source-baseline、不修改真实用户数据、不全量重装系统。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 04 WebOps UI v5 功能矩阵
|
||||
|
||||
| 页面 | 能看 | 能决策 | 能修改 | 审批 | 说明 |
|
||||
|---|---|---|---|---|---|
|
||||
| /dashboard | 是 | 是 | 否 | 否 | 总览域名、节点、路由、角色 |
|
||||
| /nodes | 是 | 是 | 是 | 中高风险需审批 | 修改节点健康状态 |
|
||||
| /roles | 是 | 否 | 否 | 否 | 只展示 owner/admin/user |
|
||||
| /routing | 是 | 是 | 是 | admin 需 owner 审批 | 修改角色路由 active_node |
|
||||
| /failover | 是 | 是 | 是 | 必须审批 | 预演、申请切换、申请回切 |
|
||||
| /approvals | 是 | 是 | 是 | owner | 审批中心 |
|
||||
| /operations | 是 | 是 | 是 | 高风险审批 | 备份、健康检查、证据、回滚 |
|
||||
| /users | 是 | 是 | 是 | 授予 owner 仅 owner | 用户管理 |
|
||||
| /quota | 是 | 是 | 是 | 按策略 | 额度管理 |
|
||||
| /models | 是 | 是 | 申请 | 是 | 模型策略申请 |
|
||||
| /memory | 是 | 是 | 软删除需审批 | 是 | 记忆导出、压缩、删除 |
|
||||
| /channels/wechat | 是 | 是 | 是 | 否 | owner 微信通知队列 |
|
||||
| /life | 是 | 是 | 申请 | 是 | 数字生命体诊断与计划 |
|
||||
| /patches | 是 | 是 | 申请 | 是 | ChatGPT/Codex 补丁协作 |
|
||||
| /optimization | 是 | 是 | 申请 | 是 | 自优化建议 |
|
||||
| /simulation | 是 | 是 | 是 | 否 | 安全仿真,不触达真实用户 |
|
||||
| /logs | 是 | 否 | 否 | 否 | 审计和事件 |
|
||||
| /config | 是 | 否 | 否 | 否 | 域名、角色、节点 |
|
||||
| /uat | 是 | 是 | 是 | 否 | 运行验收 |
|
||||
| /versions | 是 | 是 | 发布需审批 | 是 | 版本记录和发布 |
|
||||
| /evidence | 是 | 是 | 是 | 否 | 导出证据 |
|
||||
@@ -0,0 +1,21 @@
|
||||
# WebOps UI 功能矩阵
|
||||
|
||||
| 页面 | 能看 | 能决策 | 能修改 |
|
||||
|---|---|---|---|
|
||||
| Dashboard | 是 | 部分 | 否 |
|
||||
| Nodes | 是 | owner/admin | 高危审批 |
|
||||
| Roles | 是 | owner | owner |
|
||||
| Routing | 是 | owner/admin | owner 或审批后 |
|
||||
| Failover | 是 | owner/admin | owner 审批后 |
|
||||
| Approvals | 是 | owner | owner |
|
||||
| Operations | 是 | owner/admin | 高危审批 |
|
||||
| Users | 是 | owner/admin | 边界受控 |
|
||||
| Quota | 是 | owner/admin | owner/admin |
|
||||
| Models | 是 | owner/admin | owner 审批后 |
|
||||
| Memory | 是 | owner/admin | 删除需审批 |
|
||||
| WeChat | 是 | owner/admin | 高危审批 |
|
||||
| Optimization | 是 | owner/admin | owner 审批后 |
|
||||
| Simulation | 是 | owner/admin | 只 simulated |
|
||||
| Logs | 是 | 否 | 否 |
|
||||
| Config | 是 | 否 | 否 |
|
||||
| UAT | 是 | owner/admin | 只生成结果/建议 |
|
||||
@@ -0,0 +1,43 @@
|
||||
# 06 AIOS v23 v5 验收测试文档
|
||||
|
||||
## 1. 微信用户行为测试
|
||||
|
||||
| TC_ID | 输入 | 预期 |
|
||||
|---|---|---|
|
||||
| WX-TEXT-001 | user 发送普通文字 | 文本路线,正常回复 |
|
||||
| WX-TEXT-002 | user 引用普通文字后回复 | 文本路线,不进入图片识别 |
|
||||
| WX-TEXT-003 | “看一下这个方案是否合理” | 文本路线,不误判图片 |
|
||||
| WX-IMAGE-001 | user 发送图片 | 图片路线 |
|
||||
| WX-IMAGE-002 | user 引用图片追问 | 图片路线 |
|
||||
| WX-FILE-001 | user 引用 PDF | 文件路线,不进入图片识别 |
|
||||
| WX-FILE-002 | user 引用表格 | 文件路线,不进入图片识别 |
|
||||
| WX-FILE-003 | user 引用文档 | 文件路线,不进入图片识别 |
|
||||
| WX-ERR-001 | 缓存图片失效 | 友好提示,不显示内部字段 |
|
||||
|
||||
## 2. Web 用户行为测试
|
||||
|
||||
| TC_ID | 场景 | 预期 |
|
||||
|---|---|---|
|
||||
| WEB-REG-001 | user 注册 | 状态 pending,创建审批 |
|
||||
| WEB-LOGIN-001 | approved 用户登录 | 进入 dashboard |
|
||||
| ADMIN-USERS-001 | admin 修改 user 状态 | 成功,不能授予 owner |
|
||||
| ADMIN-QUOTA-001 | admin 设置额度 | 成功并审计 |
|
||||
| OWNER-APPROVAL-001 | owner 审批用户注册/高危操作 | 状态 executed |
|
||||
|
||||
## 3. 运维和自优化测试
|
||||
|
||||
| TC_ID | 场景 | 预期 |
|
||||
|---|---|---|
|
||||
| HA-001 | failover dry-run | 返回 owner/admin/user 迁移顺序 |
|
||||
| LIFE-001 | 数字生命体诊断 | 生成 signal 和 suggestion |
|
||||
| PATCH-001 | 提交补丁 | patch_queue 记录补丁 |
|
||||
| ROLLBACK-001 | 申请回滚 | 进入 approval 或 operations |
|
||||
|
||||
## 4. 自动修复策略
|
||||
|
||||
验收失败时:
|
||||
|
||||
1. 运行 `self_diagnose_v5.py`。
|
||||
2. 对安全项运行 `self_optimize_v5.py --safe-only`。
|
||||
3. 重新运行 UAT。
|
||||
4. 仍失败则标记阻塞并回滚。
|
||||
@@ -0,0 +1,29 @@
|
||||
# AIOS v23 全量验收测试文档
|
||||
|
||||
## 验收范围
|
||||
|
||||
- 微信用户行为:文字、图片、文件、引用、命令、异常。
|
||||
- Web 用户行为:注册、登录、查看自身信息。
|
||||
- admin Web 操作:审核、用户管理、额度、日志、UAT、仿真。
|
||||
- owner Web 操作:审批、failover、routing、模型、记忆、回滚、自优化。
|
||||
- HA:server-hw/server-local 健康、切换、回切、可回滚。
|
||||
- 安全:脱敏、角色隔离、文件/图片/文字路线隔离。
|
||||
|
||||
## 核心用例
|
||||
|
||||
| TC_ID | 输入 | 预期 |
|
||||
|---|---|---|
|
||||
| WX-TEXT-001 | user 微信普通文字 | 文本路线,正常回复 |
|
||||
| WX-TEXT-002 | user 引用普通文字 | 文本路线,不进 Vision |
|
||||
| WX-IMAGE-001 | user 发送图片 | Vision 路线 |
|
||||
| WX-FILE-001 | user 引用 PDF | 文件/文本路线,不进 Vision |
|
||||
| WX-FILE-002 | user 引用 Excel | 文件/文本路线,不进 Vision |
|
||||
| WEB-REG-001 | 访问注册页 | 创建 pending user |
|
||||
| WEB-LOGIN-001 | approved user 登录 | 登录成功 |
|
||||
| ADMIN-USERS-001 | admin 审核用户 | 审核成功并审计 |
|
||||
| ADMIN-QUOTA-001 | admin 改 user 额度 | 生效并审计 |
|
||||
| OWNER-APPROVAL-001 | owner 审批高危动作 | 审批成功并执行 |
|
||||
| HA-001 | server-hw down | active_node 切换到 server-local |
|
||||
| OPT-001 | UAT 失败 | 生成自优化建议 |
|
||||
|
||||
P0/P1 必须阻塞上线。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 07 ChatGPT 与 Codex 协同规则
|
||||
|
||||
## 1. ChatGPT 权限
|
||||
|
||||
ChatGPT 允许在本包中读写:
|
||||
|
||||
- 文档
|
||||
- 配置模板
|
||||
- 补丁说明
|
||||
- UAT 用例
|
||||
- Patch Studio 补丁内容
|
||||
|
||||
ChatGPT 不直接在生产执行高危操作。
|
||||
|
||||
## 2. Codex 权限
|
||||
|
||||
Codex 负责:
|
||||
|
||||
- 执行静默安装
|
||||
- 调用 OpenClaw 多 agent 并行部署
|
||||
- 应用修正脚本
|
||||
- 执行 UAT
|
||||
- 回写基线
|
||||
- 生成证据
|
||||
|
||||
## 3. 补丁流程
|
||||
|
||||
```text
|
||||
ChatGPT 生成补丁/文档/配置
|
||||
↓
|
||||
/patches 提交 patch_queue
|
||||
↓
|
||||
Codex 读取并落地到工程文件
|
||||
↓
|
||||
owner 审批
|
||||
↓
|
||||
OpenClaw 执行
|
||||
↓
|
||||
UAT + 证据 + 可回滚
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# 自优化与回滚设计
|
||||
|
||||
自进化大脑默认只做诊断、建议、白名单安全修复、复测、生成 owner 审批。不得自动删除用户数据、修改真实模型计费策略、重启核心生产服务、清空记忆或未审批 failover。
|
||||
|
||||
回滚入口:
|
||||
|
||||
```bash
|
||||
bash scripts/rollback_last_delta.sh
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
# 域名与路由规划
|
||||
|
||||
| 入口 | 域名 |
|
||||
|---|---|
|
||||
| 用户门户 | `https://cnnetmkt.top` |
|
||||
| WebOps | `https://ops.cnnetmkt.top` |
|
||||
| API | `https://api.cnnetmkt.top` |
|
||||
| 微信回调 | `https://wechat.cnnetmkt.top` |
|
||||
|
||||
所有配置和 UI 使用 `server-hw`、`server-local`。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 基线保留策略
|
||||
|
||||
最新实际部署包原样保留在:
|
||||
|
||||
```text
|
||||
source-baseline/current_actual_deployment_source.zip
|
||||
```
|
||||
|
||||
任何 agent 不得修改该文件。生产只执行 `delta-fix/` 修正层。
|
||||
@@ -0,0 +1,57 @@
|
||||
# AIOS V1R1C0 全量架构设计:MultiUser Runtime + GitOps + Custom LLM
|
||||
|
||||
## 1. 版本定位
|
||||
|
||||
本版版本号:`V1R1C0`。按照 `VxRxCx` 规则,本版属于 **R 级功能版本升级**:在现有 V1 架构不整体推翻的前提下,增加多用户 runtime 选择、在线 GitOps 基线、自定义 LLM 模型注册与优先级控制。
|
||||
|
||||
## 2. 架构原则
|
||||
|
||||
1. **复用优先**:NPM 继续作为入口代理;Casdoor/WebOps 继续作为身份权限;OpenClaw 继续作为执行器;Hermes 继续作为规划与自然语言理解层。
|
||||
2. **Git 为单一真相源**:ZIP 仅作为初始导入源,后续统一通过 branch/commit/tag/patch。
|
||||
3. **模型密钥不进用户容器**:用户自定义 LLM 的 API Key 只保存为 secret_ref。
|
||||
4. **shared/dedicated 混合 runtime**:Regular 默认 shared,Pro/Ultra/Owner/Codex/高危工具可 dedicated 或 sandbox。
|
||||
5. **用户可控模型优先级**:用户可在基础模型之外添加自定义 LLM,并决定 fallback 顺序。
|
||||
|
||||
## 3. 目标逻辑架构
|
||||
|
||||
```text
|
||||
NPM / cnnetmkt.top
|
||||
├─ / WebOps Portal
|
||||
├─ /git Git Web UI
|
||||
├─ /base 当前基线
|
||||
├─ /ver VxRxCx 版本
|
||||
├─ /diff 版本差异
|
||||
├─ /patch Patch 管理
|
||||
├─ /model 模型注册与优先级
|
||||
├─ /run Runtime 状态
|
||||
└─ /api GitOps + Model + Runtime API
|
||||
↓
|
||||
WebOps / Casdoor 权限
|
||||
↓
|
||||
Hermes NL / Risk / Planner
|
||||
↓
|
||||
OpenClaw Executor
|
||||
↓
|
||||
Runtime Selector
|
||||
↓
|
||||
Model Router
|
||||
```
|
||||
|
||||
## 4. 从 MultiUserClaw 借鉴但不照搬
|
||||
|
||||
| 借鉴点 | AIOS 落地方式 |
|
||||
|---|---|
|
||||
| Platform Gateway | 复用 WebOps/Router Gateway,不新增独立用户体系 |
|
||||
| dedicated/shared runtime | Regular shared,Pro/Ultra/Owner dedicated,高危工具 sandbox |
|
||||
| LLM key gateway 托管 | One API / Secret Store / GitOps API 保存 secret_ref |
|
||||
| Agent 模板下发 | 纳入 WebOps Agent Template Registry |
|
||||
| API token 调用 Agent | token 绑定 user_id、role、tier、scope、expire_at |
|
||||
| 容器生命周期 | 由 OpenClaw Executor 或受限 Docker Proxy 管理,不让普通 Gateway 挂 docker.sock |
|
||||
|
||||
## 5. 数据面隔离
|
||||
|
||||
- 用户模型配置按 username 隔离。
|
||||
- 用户 priority 只影响本人。
|
||||
- 全局基础模型只允许 owner/admin 调整。
|
||||
- 自定义模型 API Key 不写入 Git,不写入普通配置文件。
|
||||
- runtime selector 不允许普通用户直接选择宿主机执行器。
|
||||
@@ -0,0 +1,29 @@
|
||||
# AIOS V1R1C0 用户需求规格说明
|
||||
|
||||
## Owner
|
||||
|
||||
- 查看全局基线版本、版本差异和发布历史。
|
||||
- 审批 V 级架构变化、R 级高风险功能变化和高危 runtime 操作。
|
||||
- 管理基础模型池和角色默认模型策略。
|
||||
- 回滚到任意 VxRxCx 版本。
|
||||
|
||||
## Admin
|
||||
|
||||
- 创建和审核 patch branch。
|
||||
- 管理 Regular/Pro/Ultra 用户策略。
|
||||
- 运行 UAT 和 smoke test。
|
||||
- 查看模型路由决策和失败原因。
|
||||
|
||||
## User
|
||||
|
||||
- 使用系统预置基础模型。
|
||||
- 在基础模型之外添加自己的自定义 LLM。
|
||||
- 设置模型优先级,例如:我的本地 qwen → deepseekv4-flash → qwen3.6-35b。
|
||||
- 模型不可用时自动 fallback 到下一优先级模型。
|
||||
|
||||
## Codex / ChatGPT
|
||||
|
||||
- 通过 GitOps API 创建 branch、修改文件、提交 commit、生成 tag。
|
||||
- 通过 NL API 解释自然语言指令。
|
||||
- 使用 `/api/model/*` 接口管理模型注册与优先级。
|
||||
- 所有写操作必须产生审计记录。
|
||||
@@ -0,0 +1,79 @@
|
||||
# AIOS V1R1C0 接口规格:GitOps + custom LLM + Runtime Selector
|
||||
|
||||
默认本地服务:`127.0.0.1:18890`。
|
||||
|
||||
## 1. 认证
|
||||
|
||||
写接口必须带:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <AIOS_GITOPS_TOKEN>
|
||||
X-AIOS-User: <username>
|
||||
X-AIOS-Role: owner|admin|user
|
||||
```
|
||||
|
||||
## 2. GitOps 接口
|
||||
|
||||
| 方法 | 路径 | 功能 |
|
||||
|---|---|---|
|
||||
| GET | `/api/status` | 查看 GitOps 状态 |
|
||||
| GET | `/api/base` | 当前 baseline |
|
||||
| GET | `/api/ver` | 版本列表 |
|
||||
| GET | `/api/diff?from=&to=` | 版本差异 |
|
||||
| GET | `/api/patch` | Patch 列表 |
|
||||
| POST | `/api/branch` | 创建分支 |
|
||||
| POST | `/api/file` | 写文件并 commit |
|
||||
| POST | `/api/commit` | 提交已有变更 |
|
||||
| POST | `/api/tag` | 创建 VxRxCx tag |
|
||||
| POST | `/api/rollback` | checkout 指定版本 |
|
||||
| POST | `/api/nl` | 自然语言路由 |
|
||||
|
||||
## 3. custom LLM 接口
|
||||
|
||||
### 添加用户自定义模型
|
||||
|
||||
`POST /api/model/custom/add`
|
||||
|
||||
```json
|
||||
{
|
||||
"provider_name": "my-local-oneapi",
|
||||
"base_url": "http://127.0.0.1:13000/v1",
|
||||
"model_name": "my-qwen-local",
|
||||
"display_name": "我的本地 Qwen",
|
||||
"api_key": "只在请求中出现,服务端转为 secret_ref",
|
||||
"context_window": 32768,
|
||||
"supports_tools": true,
|
||||
"supports_vision": false,
|
||||
"supports_reasoning": true,
|
||||
"priority": 10
|
||||
}
|
||||
```
|
||||
|
||||
### 设置模型优先级
|
||||
|
||||
`POST /api/model/priority/set`
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{"model_id": "custom.alice.abc", "priority": 1},
|
||||
{"model_id": "base.deepseekv4-flash", "priority": 20},
|
||||
{"model_id": "base.qwen3.6-35b", "priority": 30}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 解析实际模型路由
|
||||
|
||||
`POST /api/model/route/resolve`
|
||||
|
||||
系统按用户 priority 排序,选择第一个可用模型;如果没有用户配置,则退回角色默认基础模型。
|
||||
|
||||
## 4. Runtime 接口
|
||||
|
||||
| 方法 | 路径 | 功能 |
|
||||
|---|---|---|
|
||||
| POST | `/api/runtime/select` | 根据 role/tier/risk 选择 shared/dedicated/sandbox |
|
||||
| POST | `/api/runtime/register` | 注册 runtime 实例 |
|
||||
| POST | `/api/runtime/heartbeat` | runtime 心跳 |
|
||||
| GET | `/api/runtime/status` | runtime 列表 |
|
||||
@@ -0,0 +1,23 @@
|
||||
# Codex 执行说明:AIOS V1R1C0 全量刷新部署
|
||||
|
||||
## 执行目标
|
||||
|
||||
把 AIOS v23/v5 WebOps 包升级为 `AIOS V1R1C0 MultiUser GitOps Custom LLM`。
|
||||
|
||||
## 严格约束
|
||||
|
||||
- 优先复用 NPM,不手写裸 Nginx。
|
||||
- 优先复用 WebOps/Casdoor,不新增用户体系。
|
||||
- 优先复用 OpenClaw/Hermes,不新增调度系统。
|
||||
- Git 服务如果已有则复用;没有才新增 Gitea。
|
||||
- 不允许把 LLM API Key 写入 Git。
|
||||
- 不允许普通 Gateway 直接挂载 docker.sock。
|
||||
|
||||
## 推荐步骤
|
||||
|
||||
```bash
|
||||
unzip AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip
|
||||
cd AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706
|
||||
bash scripts/install_v1r1c0_gitops_model_overlay.sh
|
||||
bash tests/run_v1r1c0_acceptance.sh
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
# AIOS V1R1C0 测试与验收方案
|
||||
|
||||
自动测试:
|
||||
|
||||
```bash
|
||||
bash tests/run_v1r1c0_acceptance.sh
|
||||
```
|
||||
|
||||
覆盖:API server、添加自定义模型、设置优先级、模型路由、runtime selector、branch、file commit、包完整性。
|
||||
|
||||
失败标准:API Key 明文进入 Git、写接口无认证、V 级发布无需 owner 审批、用户可修改他人模型、NPM 被绕过直接暴露服务。
|
||||
@@ -0,0 +1,7 @@
|
||||
# AIOS V1R1C0 安全与权限设计
|
||||
|
||||
- 用户 API Key 转为 secret file + secret_ref,不进入 Git。
|
||||
- 普通用户只能修改自己的模型和优先级。
|
||||
- owner 才能发 V 级版本 tag。
|
||||
- Docker 操作只能通过 OpenClaw Executor 或受限 Docker Proxy。
|
||||
- 所有模型、Git、runtime 操作都写审计日志。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 用户自定义 LLM 与模型优先级设计
|
||||
|
||||
基础模型:`base.deepseekv4-flash`、`base.qwen3.6-35b`、`base.gpt-5.5`。
|
||||
|
||||
用户自定义模型支持 OpenAI-compatible provider。API Key 只写入 secret 文件,DB 只保存 `secret://llm/<user>/<id>`。
|
||||
|
||||
路由顺序:用户显式 priority → 用户启用自定义模型 → 角色默认基础模型 → 系统兜底模型。
|
||||
|
||||
默认 fallback 策略:`try_next_on_error`。
|
||||
@@ -0,0 +1,7 @@
|
||||
# NPM 短链接接入说明
|
||||
|
||||
统一入口:`https://cnnetmkt.top`。
|
||||
|
||||
短路径:`/git /base /ver /diff /patch /model /run /api /hook`。
|
||||
|
||||
通过 Nginx Proxy Manager 的 Custom Locations 接入,不覆盖原 NPM 数据。
|
||||
@@ -0,0 +1,7 @@
|
||||
# MultiUser Runtime Selector 设计
|
||||
|
||||
模式:shared、dedicated、ephemeral_sandbox、fallback_openclaw。
|
||||
|
||||
规则:owner/admin/ultra → dedicated;pro → dedicated_or_shared;regular → shared;high_tool/shell/docker/write_prod → ephemeral_sandbox。
|
||||
|
||||
用户容器不持有模型 API Key,高危工具进入 sandbox。
|
||||
@@ -0,0 +1,7 @@
|
||||
# GitOps 在线基线与 VxRxCx 版本体系
|
||||
|
||||
当前版本:`V1R1C0`。
|
||||
|
||||
`V1R0C0` 是 ZIP 导入基线;`V1R1C0` 是 MultiUser GitOps + Custom LLM 功能升级。
|
||||
|
||||
发布流程:branch → commit → diff → test → tag → rollout → baseline update。
|
||||
@@ -0,0 +1,14 @@
|
||||
# AIOS V1R1C0 部署 Runbook
|
||||
|
||||
```bash
|
||||
unzip AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip
|
||||
cd AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706
|
||||
bash scripts/install_v1r1c0_gitops_model_overlay.sh
|
||||
bash tests/run_v1r1c0_acceptance.sh
|
||||
```
|
||||
|
||||
查看 token:
|
||||
|
||||
```bash
|
||||
cat /data/aios-gitops/runtime/secrets/gitops.tokens
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Codex 完整执行指令:AIOS V1R1C0 MultiUser GitOps Custom LLM
|
||||
|
||||
你需要基于服务器当前实际情况,完成 AIOS V1R1C0 全量架构刷新与部署。
|
||||
|
||||
## 总体目标
|
||||
|
||||
1. 在线 GitOps 基线。
|
||||
2. VxRxCx 版本管理。
|
||||
3. NPM 短链接入口。
|
||||
4. MultiUser shared/dedicated runtime selector。
|
||||
5. 用户自定义 LLM 模型添加。
|
||||
6. 用户自主设置模型优先级。
|
||||
7. OpenClaw 命令适配。
|
||||
8. Hermes 自然语言意图适配。
|
||||
9. 全量接口测试与验收报告。
|
||||
|
||||
## 强制复用
|
||||
|
||||
优先复用:NPM、WebOps、Casdoor、OpenClaw、Hermes、One API。
|
||||
|
||||
禁止优先新增:新的用户系统、新的入口代理、新的调度平台、普通 Gateway 直接 docker.sock。
|
||||
|
||||
## 执行步骤
|
||||
|
||||
```bash
|
||||
unzip AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip
|
||||
cd AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706
|
||||
bash scripts/install_v1r1c0_gitops_model_overlay.sh
|
||||
bash tests/run_v1r1c0_acceptance.sh
|
||||
```
|
||||
|
||||
## NPM 短链接
|
||||
|
||||
`/git /api /base /ver /diff /patch /model /run /hook`。
|
||||
|
||||
## 成功输出
|
||||
|
||||
```text
|
||||
AIOS_V1R1C0_STATUS=<SUCCESS|FAILED|PARTIAL>
|
||||
DOMAIN=https://cnnetmkt.top
|
||||
VERSION=V1R1C0
|
||||
SHORT_LINKS=/git,/base,/ver,/diff,/patch,/model,/run,/api,/hook
|
||||
CUSTOM_LLM_API=<OK|FAILED|PARTIAL>
|
||||
MODEL_PRIORITY=<OK|FAILED|PARTIAL>
|
||||
RUNTIME_SELECTOR=<OK|FAILED|PARTIAL>
|
||||
NEXT_UPDATE_MODE=git_patch_only
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
# ChatGPT / Codex GitOps Access
|
||||
|
||||
Entry: https://cnnetmkt.top
|
||||
|
||||
Short links: /git, /base, /ver, /diff, /patch, /model, /run, /api, /hook.
|
||||
|
||||
All writes must become Git commits. Fixes must use patch branches. Releases must use VxRxCx tags.
|
||||
|
||||
Forbidden:
|
||||
|
||||
- direct production edits without Git history
|
||||
- API keys in Git
|
||||
- API keys in plaintext logs
|
||||
- bypassing owner/admin approval for V-level changes
|
||||
@@ -0,0 +1,13 @@
|
||||
# AIOS VxRxCx Versioning Rules
|
||||
|
||||
VxRxCx is the only supported online baseline version format.
|
||||
|
||||
- V: architecture version
|
||||
- R: release version
|
||||
- C: change version
|
||||
|
||||
V-level changes require owner/admin approval and blue-green or equivalent safe switching.
|
||||
R-level changes require canary or test-environment validation.
|
||||
C-level changes require smoke test and automatic rollback on failure.
|
||||
|
||||
Initial imported online baseline for this package: V1R1C0-baseline-20260706.
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys, pathlib, json
|
||||
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else '.').resolve(); (runtime/'.aios_patch_manifests').mkdir(exist_ok=True)
|
||||
conf=runtime/'webops'/'config'; conf.mkdir(parents=True,exist_ok=True)
|
||||
(conf/'role_model.active.json').write_text(json.dumps({'roles':['owner','admin','user'],'legacy_role_mapping':{'ultra':'owner','pro':'admin','regular':'user'},'rule':'active runtime authorization must use owner/admin/user only'},ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
(runtime/'AIOS_ACTIVE_ROLE_MODEL_OWNER_ADMIN_USER').write_text('owner/admin/user\n',encoding='utf-8')
|
||||
print('ROLE_CONVERGENCE_PATCH_OK', runtime)
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import sys, json, shutil
|
||||
root=Path(sys.argv[1]) if len(sys.argv)>1 else Path.cwd()
|
||||
report={'root':str(root),'created':[],'needs_codex_binding':[]}
|
||||
patch_dir=root/'runtime_patches'
|
||||
patch_dir.mkdir(exist_ok=True)
|
||||
guard=patch_dir/'attachment_reference_route_guard.py'
|
||||
guard.write_text('''# AIOS v5 route guard
|
||||
IMAGE_EXT={".jpg",".jpeg",".png",".webp",".gif"}
|
||||
FILE_EXT={".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".txt",".csv",".md",".zip",".rar",".7z"}
|
||||
def classify_message(message_type="text", filename="", mime="", content=""):
|
||||
t=(message_type or "").lower(); f=(filename or content or "").lower(); m=(mime or "").lower()
|
||||
if t in ("image","photo","quote_image") or m.startswith("image/") or any(f.endswith(x) for x in IMAGE_EXT): return "vision"
|
||||
if t in ("file","quote_file") or any(f.endswith(x) for x in FILE_EXT) or (m and not m.startswith("image/")): return "file"
|
||||
return "text"
|
||||
def expose_route_proof(role, debug=False):
|
||||
return bool(debug and role == "owner")
|
||||
''', encoding='utf-8')
|
||||
report['created'].append(str(guard))
|
||||
patterns=['hasHermesReferenceContext','findHermesReferencedMediaItem','cached_image_expired']
|
||||
for f in list(root.rglob('*.py'))+list(root.rglob('*.js'))+list(root.rglob('*.ts')):
|
||||
if 'runtime_patches' in str(f) or 'source-baseline' in str(f): continue
|
||||
try: txt=f.read_text(encoding='utf-8', errors='ignore')
|
||||
except Exception: continue
|
||||
if any(p in txt for p in patterns):
|
||||
backup=f.with_suffix(f.suffix+'.v5guard.bak')
|
||||
if not backup.exists(): shutil.copy2(f, backup)
|
||||
if 'AIOS_V5_ROUTE_GUARD_REVIEW_REQUIRED' not in txt:
|
||||
f.write_text(txt+'\n\n# AIOS_V5_ROUTE_GUARD_REVIEW_REQUIRED: bind runtime_patches.attachment_reference_route_guard.classify_message before Vision routing.\n', encoding='utf-8')
|
||||
report['needs_codex_binding'].append(str(f))
|
||||
(root/'runtime_patches'/'v5_patch_report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2), encoding='utf-8')
|
||||
print(json.dumps(report,ensure_ascii=False,indent=2))
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, display_name TEXT, role TEXT NOT NULL CHECK(role IN ('owner','admin','user')), status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','disabled')), password_hash TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS node_status(node_id TEXT PRIMARY KEY CHECK(node_id IN ('server-hw','server-local')), health TEXT NOT NULL DEFAULT 'unknown', active INTEGER NOT NULL DEFAULT 0, last_seen_at TEXT, score INTEGER NOT NULL DEFAULT 0, note TEXT);
|
||||
CREATE TABLE IF NOT EXISTS routing_policy(role TEXT PRIMARY KEY CHECK(role IN ('owner','admin','user')), primary_node TEXT NOT NULL CHECK(primary_node IN ('server-hw','server-local')), backup_node TEXT NOT NULL CHECK(backup_node IN ('server-hw','server-local')), active_node TEXT NOT NULL CHECK(active_node IN ('server-hw','server-local')), failover_priority INTEGER NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS approvals(id TEXT PRIMARY KEY, action TEXT NOT NULL, payload TEXT NOT NULL, requested_by TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','executed','failed')), risk TEXT NOT NULL DEFAULT 'medium', created_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT, result TEXT);
|
||||
CREATE TABLE IF NOT EXISTS operations(id TEXT PRIMARY KEY, op_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','running','succeeded','failed','skipped')), created_by TEXT NOT NULL, created_at TEXT NOT NULL, started_at TEXT, finished_at TEXT, result TEXT);
|
||||
CREATE TABLE IF NOT EXISTS audit_logs(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, target TEXT, payload TEXT, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS events(id TEXT PRIMARY KEY, channel TEXT NOT NULL, actor TEXT, event_type TEXT NOT NULL, payload TEXT NOT NULL, route_target TEXT, status TEXT NOT NULL DEFAULT 'received', created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS quota_policy(username TEXT PRIMARY KEY, daily_limit INTEGER NOT NULL DEFAULT 100, used_today INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS model_policy(role TEXT PRIMARY KEY CHECK(role IN ('owner','admin','user')), default_model TEXT NOT NULL, allowed_models TEXT NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS memory_items(id TEXT PRIMARY KEY, username TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'user', content TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS owner_notifications(id TEXT PRIMARY KEY, event_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', created_at TEXT NOT NULL, sent_at TEXT);
|
||||
CREATE TABLE IF NOT EXISTS self_optimization_suggestions(id TEXT PRIMARY KEY, category TEXT NOT NULL, severity TEXT NOT NULL, title TEXT NOT NULL, detail TEXT NOT NULL, proposed_fix TEXT, safe_auto_fix INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'open', created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS uat_results(id TEXT PRIMARY KEY, run_id TEXT NOT NULL, tc_id TEXT NOT NULL, category TEXT NOT NULL, status TEXT NOT NULL, latency_ms INTEGER, detail TEXT, created_at TEXT NOT NULL);
|
||||
@@ -0,0 +1,5 @@
|
||||
INSERT OR IGNORE INTO node_status(node_id, health, active, score, last_seen_at, note) VALUES ('server-hw','ok',1,100,datetime('now'),'primary logical node'),('server-local','ok',0,95,datetime('now'),'backup logical node');
|
||||
INSERT OR IGNORE INTO routing_policy(role, primary_node, backup_node, active_node, failover_priority, updated_at) VALUES ('owner','server-hw','server-local','server-hw',1,datetime('now')),('admin','server-hw','server-local','server-hw',2,datetime('now')),('user','server-hw','server-local','server-hw',3,datetime('now'));
|
||||
INSERT OR IGNORE INTO users(id, username, display_name, role, status, password_hash, created_at, updated_at) VALUES ('bootstrap-owner','owner','Bootstrap Owner','owner','approved','bootstrap-change-me',datetime('now'),datetime('now')),('bootstrap-admin','admin','Bootstrap Admin','admin','approved','bootstrap-change-me',datetime('now'),datetime('now')),('sample-user','sample-user','Sample User','user','approved','bootstrap-change-me',datetime('now'),datetime('now'));
|
||||
INSERT OR IGNORE INTO model_policy(role, default_model, allowed_models, updated_at) VALUES ('owner','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b","gpt-5.5"]',datetime('now')),('admin','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b"]',datetime('now')),('user','deepseekv4-flash','["deepseekv4-flash"]',datetime('now'));
|
||||
INSERT OR IGNORE INTO quota_policy(username, daily_limit, used_today, updated_at) VALUES ('owner',10000,0,datetime('now')),('admin',2000,0,datetime('now')),('sample-user',200,0,datetime('now'));
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS gitops_baseline(
|
||||
id TEXT PRIMARY KEY,
|
||||
active_version TEXT NOT NULL,
|
||||
active_tag TEXT NOT NULL,
|
||||
source_package TEXT,
|
||||
entry TEXT NOT NULL,
|
||||
update_mode TEXT NOT NULL DEFAULT 'git_patch_only',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS gitops_versions(version TEXT PRIMARY KEY, tag TEXT NOT NULL, level TEXT NOT NULL CHECK(level IN ('V','R','C','baseline')), summary TEXT, status TEXT NOT NULL DEFAULT 'draft', created_by TEXT NOT NULL DEFAULT 'system', created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS gitops_patches(id TEXT PRIMARY KEY, patch_name TEXT NOT NULL, branch TEXT NOT NULL, base_ref TEXT NOT NULL DEFAULT 'main', target_version TEXT, risk TEXT NOT NULL DEFAULT 'medium', status TEXT NOT NULL DEFAULT 'draft', created_by TEXT NOT NULL, created_at TEXT NOT NULL, applied_at TEXT, result TEXT);
|
||||
CREATE TABLE IF NOT EXISTS llm_providers(id TEXT PRIMARY KEY, owner_username TEXT NOT NULL, name TEXT NOT NULL, provider_type TEXT NOT NULL DEFAULT 'openai_compatible', base_url TEXT NOT NULL, auth_mode TEXT NOT NULL DEFAULT 'bearer', api_key_secret_ref TEXT, scope TEXT NOT NULL DEFAULT 'user' CHECK(scope IN ('global','role','user')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS llm_models(id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, owner_username TEXT NOT NULL, model_name TEXT NOT NULL, display_name TEXT, context_window INTEGER DEFAULT 0, input_modalities TEXT NOT NULL DEFAULT '["text"]', output_modalities TEXT NOT NULL DEFAULT '["text"]', supports_tools INTEGER NOT NULL DEFAULT 0, supports_vision INTEGER NOT NULL DEFAULT 0, supports_reasoning INTEGER NOT NULL DEFAULT 0, cost_tier TEXT NOT NULL DEFAULT 'custom', is_base INTEGER NOT NULL DEFAULT 0, scope TEXT NOT NULL DEFAULT 'user' CHECK(scope IN ('global','role','user')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(owner_username, provider_id, model_name));
|
||||
CREATE TABLE IF NOT EXISTS user_model_priority(username TEXT NOT NULL, model_id TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 100, enabled INTEGER NOT NULL DEFAULT 1, fallback_policy TEXT NOT NULL DEFAULT 'try_next_on_error', updated_at TEXT NOT NULL, PRIMARY KEY(username, model_id));
|
||||
CREATE TABLE IF NOT EXISTS model_route_decisions(id TEXT PRIMARY KEY, username TEXT NOT NULL, task_type TEXT NOT NULL, selected_model_id TEXT, candidate_order TEXT NOT NULL, reason TEXT NOT NULL, created_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS user_runtime_profiles(username TEXT PRIMARY KEY, role TEXT NOT NULL DEFAULT 'user', tier TEXT NOT NULL DEFAULT 'regular', runtime_mode TEXT NOT NULL DEFAULT 'shared', allow_dedicated INTEGER NOT NULL DEFAULT 0, allow_custom_llm INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS runtime_instances(id TEXT PRIMARY KEY, username TEXT NOT NULL, runtime_type TEXT NOT NULL, backend TEXT NOT NULL DEFAULT 'hermes', status TEXT NOT NULL DEFAULT 'registered', endpoint TEXT, last_seen_at TEXT, created_at TEXT NOT NULL, metadata TEXT NOT NULL DEFAULT '{}');
|
||||
CREATE TABLE IF NOT EXISTS model_audit_logs(id TEXT PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, target TEXT, payload TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL);
|
||||
INSERT OR IGNORE INTO gitops_baseline(id,active_version,active_tag,source_package,entry,update_mode,status,created_at,updated_at) VALUES('current','V1R1C0','V1R1C0','AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706','https://cnnetmkt.top','git_patch_only','active',datetime('now'),datetime('now'));
|
||||
INSERT OR IGNORE INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES('V1R1C0','V1R1C0','R','Add MultiUser runtime selector, GitOps online baseline and user custom LLM priority registry.','ready','system',datetime('now'));
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT"
|
||||
bash scripts/package_contract_check_v1r1c0.sh
|
||||
if [ -f FULL_PACKAGE_FILE_MANIFEST.sha256 ]; then sha256sum -c FULL_PACKAGE_FILE_MANIFEST.sha256 --ignore-missing >/tmp/aios_manifest_check.log || true; fi
|
||||
echo "AIOS_V1R1C0_PACKAGE_VERIFY_OK"
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys, sqlite3, pathlib, shutil, os
|
||||
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else os.environ.get('AIOS_RUNTIME_ROOT','.')).resolve(); webops=runtime/'webops'
|
||||
(webops/'data').mkdir(parents=True,exist_ok=True); (webops/'migrations').mkdir(parents=True,exist_ok=True)
|
||||
pkg=pathlib.Path(__file__).resolve().parents[1]
|
||||
for src in sorted((pkg/'migrations').glob('*.sql')):
|
||||
dst=webops/'migrations'/src.name
|
||||
if not dst.exists(): shutil.copy2(src,dst)
|
||||
con=sqlite3.connect(webops/'data'/'webops.sqlite3')
|
||||
for sqlf in sorted((webops/'migrations').glob('*.sql')):
|
||||
con.executescript(sqlf.read_text(encoding='utf-8')); con.commit()
|
||||
con.close(); print('MIGRATIONS_APPLIED', webops/'data'/'webops.sqlite3')
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${AIOS_RUNTIME_ROOT:?AIOS_RUNTIME_ROOT is required or run from runtime root}"
|
||||
: "${AIOS_RELEASE_ID:=manual-$(date +%Y%m%dT%H%M%S)}"
|
||||
BACKUP_DIR="$AIOS_RUNTIME_ROOT/.aios_backups/$AIOS_RELEASE_ID"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
tar --exclude='.aios_backups' --exclude='source-baseline' -czf "$BACKUP_DIR/runtime-before-fix.tgz" -C "$AIOS_RUNTIME_ROOT" .
|
||||
cat > "$BACKUP_DIR/ROLLBACK.json" <<JSON
|
||||
{"release_id":"$AIOS_RELEASE_ID","backup":"$BACKUP_DIR/runtime-before-fix.tgz","created_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
|
||||
JSON
|
||||
echo "BACKUP_CREATED $BACKUP_DIR/runtime-before-fix.tgz"
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
PATCH_NAME="${1:?patch name required}"
|
||||
BASE="${2:-main}"
|
||||
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
|
||||
git fetch --all || true
|
||||
git checkout "$BASE"
|
||||
git pull origin "$BASE" || true
|
||||
git checkout -B "patch/$PATCH_NAME"
|
||||
mkdir -p "patches/$PATCH_NAME"
|
||||
cat > "patches/$PATCH_NAME/patch.json" <<EOF
|
||||
{"patch":"$PATCH_NAME","branch":"patch/$PATCH_NAME","base":"$BASE","created_at":"$(date -Is)","status":"draft"}
|
||||
EOF
|
||||
git add "patches/$PATCH_NAME/patch.json"
|
||||
git commit -m "patch($PATCH_NAME): create patch branch" || true
|
||||
git push -u origin "patch/$PATCH_NAME" || true
|
||||
echo "PATCH_CREATED patch/$PATCH_NAME"
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${AIOS_RUNTIME_ROOT:=$(pwd)}"
|
||||
OUT="${1:-$AIOS_RUNTIME_ROOT/evidence/webops-evidence-$(date +%Y%m%dT%H%M%S).tgz}"
|
||||
mkdir -p "$(dirname "$OUT")"; tar -czf "$OUT" -C "$AIOS_RUNTIME_ROOT" webops .aios_patch_manifests 2>/dev/null || tar -czf "$OUT" -C "$AIOS_RUNTIME_ROOT" webops; echo "EVIDENCE_EXPORTED $OUT"
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
import pathlib, sys, os, json, hashlib, time
|
||||
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else os.environ.get('AIOS_RUNTIME_ROOT','.')).resolve(); out=runtime/'.aios_patch_manifests'; out.mkdir(exist_ok=True)
|
||||
files=[]
|
||||
if (runtime/'webops').exists():
|
||||
for p in (runtime/'webops').rglob('*'):
|
||||
if p.is_file(): files.append({'path':str(p.relative_to(runtime)),'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'size':p.stat().st_size})
|
||||
manifest={'release_id':os.environ.get('AIOS_RELEASE_ID','manual'),'created_at':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'files':files,'roles':['owner','admin','user'],'nodes':['server-hw','server-local']}
|
||||
(out/'release_manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print('RELEASE_MANIFEST_WRITTEN', out/'release_manifest.json')
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
PORT="${AIOS_WEBOPS_PORT:-18081}"
|
||||
python3 - <<PY
|
||||
import urllib.request, os
|
||||
port=os.environ.get('AIOS_WEBOPS_PORT','18081')
|
||||
print(urllib.request.urlopen(f'http://127.0.0.1:{port}/health/ready',timeout=5).read().decode())
|
||||
PY
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
|
||||
WORKSPACE="${AIOS_GITOPS_WORKSPACE:-$ROOT/workspace}"
|
||||
ZIP_NAME="${1:-AIOS_v23_webops_owner_admin_user_PROD_DEPLOY_20260706.zip}"
|
||||
mkdir -p "$ROOT/imports" "$WORKSPACE"
|
||||
ZIP_PATH="$(find /data /opt /root /home -name "$ZIP_NAME" 2>/dev/null | head -1 || true)"
|
||||
if [ -z "$ZIP_PATH" ]; then echo "BLOCKED: missing $ZIP_NAME"; exit 10; fi
|
||||
cp "$ZIP_PATH" "$ROOT/imports/"
|
||||
cd "$WORKSPACE"
|
||||
unzip -o "$ROOT/imports/$ZIP_NAME"
|
||||
git init
|
||||
git config user.name "AIOS GitOps Bot"
|
||||
git config user.email "gitops-bot@cnnetmkt.top"
|
||||
git add .
|
||||
git commit -m "init(baseline): import $ZIP_NAME as V1R0C0" || true
|
||||
git branch -M main
|
||||
git tag -a V1R0C0-baseline-20260706 -m "V1R0C0 baseline imported from $ZIP_NAME" || true
|
||||
echo "BASELINE_IMPORT_OK workspace=$WORKSPACE zip=$ZIP_PATH"
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
PKG_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
AIOS_GITOPS_ROOT="${AIOS_GITOPS_ROOT:-/data/aios-gitops}"
|
||||
WORKSPACE="${AIOS_GITOPS_WORKSPACE:-$AIOS_GITOPS_ROOT/workspace}"
|
||||
LOG_DIR="$AIOS_GITOPS_ROOT/logs"
|
||||
BACKUP_DIR="$AIOS_GITOPS_ROOT/backups/pre-v1r1c0-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$LOG_DIR" "$BACKUP_DIR" "$WORKSPACE" "$AIOS_GITOPS_ROOT/runtime/secrets"
|
||||
LOG="$LOG_DIR/install-v1r1c0-$(date +%Y%m%d-%H%M%S).log"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
|
||||
echo "AIOS V1R1C0 GitOps + Custom LLM overlay installer"
|
||||
echo "PKG_ROOT=$PKG_ROOT"
|
||||
echo "AIOS_GITOPS_ROOT=$AIOS_GITOPS_ROOT"
|
||||
echo "WORKSPACE=$WORKSPACE"
|
||||
|
||||
echo "== Preflight =="
|
||||
hostname || true; date -Is; whoami; pwd; df -h || true; free -h || true; docker ps || true; docker compose version || docker-compose version || true; ss -lntp || true
|
||||
|
||||
echo "== Backup existing important paths =="
|
||||
cp -a /data/agent "$BACKUP_DIR/data-agent" 2>/dev/null || true
|
||||
cp -a /data/aios "$BACKUP_DIR/data-aios" 2>/dev/null || true
|
||||
cp -a /opt/aios "$BACKUP_DIR/opt-aios" 2>/dev/null || true
|
||||
cp -a /etc/systemd/system "$BACKUP_DIR/systemd-system" 2>/dev/null || true
|
||||
|
||||
echo "== Copy overlay files into GitOps root =="
|
||||
mkdir -p "$AIOS_GITOPS_ROOT/adapters" "$AIOS_GITOPS_ROOT/scripts" "$AIOS_GITOPS_ROOT/artifacts" "$AIOS_GITOPS_ROOT/runtime"
|
||||
rsync -a "$PKG_ROOT/adapters/" "$AIOS_GITOPS_ROOT/adapters/"
|
||||
rsync -a "$PKG_ROOT/scripts/" "$AIOS_GITOPS_ROOT/scripts/"
|
||||
rsync -a "$PKG_ROOT/config/" "$AIOS_GITOPS_ROOT/config/"
|
||||
|
||||
if [ ! -d "$WORKSPACE/.git" ]; then
|
||||
echo "== Initialize workspace git repository from package content =="
|
||||
rsync -a --delete --exclude 'source-baseline/*.zip' "$PKG_ROOT/" "$WORKSPACE/"
|
||||
cd "$WORKSPACE"
|
||||
git init
|
||||
git config user.name "AIOS GitOps Bot"
|
||||
git config user.email "gitops-bot@cnnetmkt.top"
|
||||
git add .
|
||||
git commit -m "init(baseline): import AIOS V1R1C0 multiuser gitops custom llm baseline" || true
|
||||
git branch -M main
|
||||
git tag -a V1R1C0 -m "AIOS V1R1C0 multiuser gitops custom llm baseline" || true
|
||||
else
|
||||
echo "== Existing workspace git repo found; overlay V1R1C0 files =="
|
||||
rsync -a "$PKG_ROOT/docs/" "$WORKSPACE/docs/"
|
||||
rsync -a "$PKG_ROOT/config/" "$WORKSPACE/config/"
|
||||
rsync -a "$PKG_ROOT/migrations/" "$WORKSPACE/migrations/"
|
||||
rsync -a "$PKG_ROOT/adapters/" "$WORKSPACE/adapters/"
|
||||
rsync -a "$PKG_ROOT/scripts/" "$WORKSPACE/scripts/"
|
||||
rsync -a "$PKG_ROOT/tests/" "$WORKSPACE/tests/"
|
||||
cd "$WORKSPACE"
|
||||
git add docs config migrations adapters scripts tests VERSION README.md || true
|
||||
git commit -m "feat(gitops): add V1R1C0 multiuser custom LLM registry and runtime selector" || true
|
||||
git tag -a V1R1C0 -m "AIOS V1R1C0 feature release" || true
|
||||
fi
|
||||
|
||||
echo "== Token initialization =="
|
||||
TOKEN_FILE="$AIOS_GITOPS_ROOT/runtime/secrets/gitops.tokens"
|
||||
if [ ! -s "$TOKEN_FILE" ]; then
|
||||
(openssl rand -hex 32 || python3 -c 'import secrets; print(secrets.token_hex(32))') > "$TOKEN_FILE"
|
||||
chmod 600 "$TOKEN_FILE"
|
||||
fi
|
||||
|
||||
echo "== Install systemd service for GitOps Model API =="
|
||||
cat >/etc/systemd/system/aios-gitops-model-api.service <<EOF
|
||||
[Unit]
|
||||
Description=AIOS V1R1C0 GitOps Model API
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=AIOS_GITOPS_ROOT=$AIOS_GITOPS_ROOT
|
||||
Environment=AIOS_GITOPS_WORKSPACE=$WORKSPACE
|
||||
Environment=AIOS_GITOPS_DB=$AIOS_GITOPS_ROOT/runtime/gitops_model.sqlite3
|
||||
Environment=AIOS_GITOPS_TOKEN_FILE=$TOKEN_FILE
|
||||
Environment=AIOS_GITOPS_HOST=127.0.0.1
|
||||
Environment=AIOS_GITOPS_PORT=18890
|
||||
ExecStart=/usr/bin/python3 $AIOS_GITOPS_ROOT/adapters/gitops/aios_gitops_model_api.py
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now aios-gitops-model-api.service
|
||||
sleep 1
|
||||
systemctl status aios-gitops-model-api.service --no-pager || true
|
||||
|
||||
echo "== NPM reminder =="
|
||||
cat <<'EOF'
|
||||
Please configure existing Nginx Proxy Manager custom locations on cnnetmkt.top:
|
||||
/git -> http://127.0.0.1:3000
|
||||
/api -> http://127.0.0.1:18890/api
|
||||
/base -> http://127.0.0.1:18890/base
|
||||
/ver -> http://127.0.0.1:18890/ver
|
||||
/diff -> http://127.0.0.1:18890/diff
|
||||
/patch -> http://127.0.0.1:18890/patch
|
||||
/run -> http://127.0.0.1:18890/run
|
||||
/model -> http://127.0.0.1:18890/model
|
||||
/hook -> existing OpenClaw/WebOps webhook, or 127.0.0.1:18891 if adapter is enabled
|
||||
EOF
|
||||
|
||||
echo "== Local health check =="
|
||||
curl -fsS http://127.0.0.1:18890/health || true
|
||||
|
||||
echo "AIOS_V1R1C0_INSTALL_DONE log=$LOG backup=$BACKUP_DIR"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
RUNTIME_ROOT="${1:-${AIOS_RUNTIME_ROOT:-$(pwd)}}"
|
||||
PKG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
mkdir -p "$RUNTIME_ROOT/webops/app" "$RUNTIME_ROOT/webops/migrations" "$RUNTIME_ROOT/webops/data" "$RUNTIME_ROOT/webops/config" "$RUNTIME_ROOT/webops/scripts"
|
||||
cp "$PKG_ROOT/active-runtime/app/aios_webops_app.py" "$RUNTIME_ROOT/webops/app/aios_webops_app.py"
|
||||
cp "$PKG_ROOT"/migrations/*.sql "$RUNTIME_ROOT/webops/migrations/"
|
||||
cp "$PKG_ROOT/config/domains.env" "$RUNTIME_ROOT/webops/config/domains.env"
|
||||
cp "$PKG_ROOT/config/roles.yaml" "$RUNTIME_ROOT/webops/config/roles.yaml"
|
||||
cp "$PKG_ROOT/scripts/run_webops_local.sh" "$RUNTIME_ROOT/webops/scripts/run_webops_local.sh"
|
||||
chmod +x "$RUNTIME_ROOT/webops/app/aios_webops_app.py" "$RUNTIME_ROOT/webops/scripts/run_webops_local.sh"
|
||||
echo "WEBOPS_OVERLAY_INSTALLED $RUNTIME_ROOT/webops"
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3, pathlib, os
|
||||
root=pathlib.Path(os.environ.get('AIOS_WEBOPS_ROOT','.')); db=root/'data'/'webops.sqlite3'
|
||||
def send_owner_wechat(event_type,payload): print('[OWNER_WECHAT_NOTIFY]',event_type,payload)
|
||||
with sqlite3.connect(db) as con:
|
||||
con.row_factory=sqlite3.Row
|
||||
rs=con.execute("SELECT * FROM owner_notifications WHERE status='queued' ORDER BY created_at LIMIT 20").fetchall()
|
||||
for r in rs:
|
||||
send_owner_wechat(r['event_type'],r['payload'])
|
||||
con.execute("UPDATE owner_notifications SET status='sent',sent_at=datetime('now') WHERE id=?",(r['id'],))
|
||||
con.commit()
|
||||
print('OWNER_WECHAT_NOTIFY_WORKER_DONE', len(rs))
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
required=("VERSION" "docs/10_ARCHITECTURE_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_CN.md" "docs/11_USER_REQUIREMENTS_V1R1C0_CN.md" "docs/12_INTERFACE_API_SPEC_V1R1C0_CN.md" "docs/CODEX_FULL_EXECUTION_PROMPT_V1R1C0.md" "active-runtime/app/aios_gitops_model_api.py" "migrations/004_gitops_custom_llm_runtime.sql" "scripts/install_v1r1c0_gitops_model_overlay.sh" "config/model_registry.yaml" "config/runtime_selector.yaml")
|
||||
for f in "${required[@]}"; do test -f "$ROOT/$f" || { echo "MISSING $f"; exit 1; }; done
|
||||
grep -q "V1R1C0" "$ROOT/VERSION"
|
||||
python3 -m py_compile "$ROOT/active-runtime/app/aios_gitops_model_api.py" "$ROOT/adapters/openclaw/openclaw_gitops_commands.py" "$ROOT/adapters/hermes/hermes_gitops_nl_router.py"
|
||||
echo "PACKAGE_CONTRACT_OK V1R1C0"
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
VERSION="${1:?version required, e.g. V1R1C0}"
|
||||
MESSAGE="${2:-release $VERSION}"
|
||||
if ! echo "$VERSION" | grep -Eq '^V[0-9]+R[0-9]+C[0-9]+$'; then echo "ERROR: version must match VxRxCx"; exit 1; fi
|
||||
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
|
||||
git checkout main
|
||||
git pull origin main || true
|
||||
git tag -a "$VERSION" -m "$MESSAGE" || true
|
||||
git push origin "$VERSION" || true
|
||||
python3 - <<PY
|
||||
import json, datetime
|
||||
from pathlib import Path
|
||||
p=Path('baseline/current.json')
|
||||
if p.exists(): d=json.loads(p.read_text())
|
||||
else: p.parent.mkdir(exist_ok=True); d={}
|
||||
d['active_version']='$VERSION'; d['active_tag']='$VERSION'; d['updated_at']=datetime.datetime.now(datetime.UTC).isoformat(); d['update_mode']='git_patch_only'
|
||||
p.write_text(json.dumps(d, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
git add baseline/current.json || true
|
||||
git commit -m "release($VERSION): update active baseline" || true
|
||||
git push origin main || true
|
||||
echo "RELEASE_DONE $VERSION"
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${AIOS_RUNTIME_ROOT:=$(pwd)}"
|
||||
BK="$(find "$AIOS_RUNTIME_ROOT/.aios_backups" -name runtime-before-fix.tgz -type f 2>/dev/null | sort | tail -1)"
|
||||
[ -n "$BK" ] || { echo "NO_BACKUP_FOUND" >&2; exit 2; }
|
||||
tmp="$AIOS_RUNTIME_ROOT/.rollback_tmp_$(date +%s)"; mkdir -p "$tmp"; tar -xzf "$BK" -C "$tmp"; rsync -a --delete --exclude='.aios_backups' "$tmp/" "$AIOS_RUNTIME_ROOT/"; rm -rf "$tmp"; echo "ROLLBACK_OK from=$BK"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
TARGET="${1:?target version required}"
|
||||
cd "${AIOS_GITOPS_WORKSPACE:-/data/aios-gitops/workspace}"
|
||||
git fetch --all --tags || true
|
||||
git checkout "$TARGET"
|
||||
if systemctl list-unit-files | grep -q aios-gitops-model-api.service; then systemctl restart aios-gitops-model-api.service; fi
|
||||
echo "ROLLBACK_OK target=$TARGET"
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${AIOS_RUNTIME_ROOT:=$(pwd)}"
|
||||
PORT="${AIOS_WEBOPS_PORT:-18081}"
|
||||
if ! curl -fsS "http://127.0.0.1:$PORT/health/ready" >/dev/null 2>&1; then
|
||||
echo "WebOps not running; starting local instance for acceptance"
|
||||
bash "$AIOS_RUNTIME_ROOT/webops/scripts/run_webops_local.sh" >/tmp/aios-webops-uat.log 2>&1 &
|
||||
PID=$!
|
||||
trap 'kill $PID >/dev/null 2>&1 || true' EXIT
|
||||
sleep 1
|
||||
fi
|
||||
python3 "$(dirname "$0")/run_wechat_behavior_simulation.py" || python3 "$(dirname "$0")/self_diagnose.py"
|
||||
python3 "$(dirname "$0")/run_full_uat.py" || {
|
||||
echo "UAT failed; running self optimization suggestion pass"
|
||||
python3 "$(dirname "$0")/self_optimize.py" || true
|
||||
exit 2
|
||||
}
|
||||
echo "ACCEPTANCE_WITH_AUTOFIX_PASS"
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
import urllib.request, urllib.parse, json, http.cookiejar, sys, os
|
||||
base=os.environ.get('AIOS_WEBOPS_BASE','http://127.0.0.1:18081')
|
||||
cj=http.cookiejar.CookieJar(); opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||
def post(path,data):
|
||||
req=urllib.request.Request(base+path,data=json.dumps(data).encode(),headers={'Content-Type':'application/json'},method='POST')
|
||||
return json.loads(opener.open(req,timeout=10).read().decode())
|
||||
def form(path,data):
|
||||
req=urllib.request.Request(base+path,data=urllib.parse.urlencode(data).encode(),headers={'Content-Type':'application/x-www-form-urlencoded'},method='POST')
|
||||
return opener.open(req,timeout=10).read().decode()
|
||||
def get(path): return opener.open(base+path,timeout=10).read().decode()
|
||||
form('/login',{'username':'owner','password':'bootstrap-change-me'})
|
||||
ready=get('/health/ready')
|
||||
assert 'owner' in ready and 'server-hw' in ready
|
||||
res=post('/api/uat/run',{})
|
||||
print(json.dumps(res,ensure_ascii=False,indent=2))
|
||||
if res.get('failed'): sys.exit(2)
|
||||
print('FULL_UAT_PASS')
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import json, os, subprocess, sys, time, urllib.request, tempfile, pathlib
|
||||
PORT = int(os.getenv('AIOS_TEST_PORT','18990'))
|
||||
ROOT = pathlib.Path(tempfile.mkdtemp(prefix='aios-v1r1c0-uat-'))
|
||||
WORKSPACE = ROOT / 'workspace'; WORKSPACE.mkdir()
|
||||
subprocess.run(['git','init'],cwd=WORKSPACE,check=True,stdout=subprocess.DEVNULL)
|
||||
subprocess.run(['git','config','user.name','AIOS UAT'],cwd=WORKSPACE,check=True)
|
||||
subprocess.run(['git','config','user.email','uat@cnnetmkt.top'],cwd=WORKSPACE,check=True)
|
||||
(WORKSPACE/'README.md').write_text('uat')
|
||||
subprocess.run(['git','add','.'],cwd=WORKSPACE,check=True)
|
||||
subprocess.run(['git','commit','-m','init'],cwd=WORKSPACE,check=True,stdout=subprocess.DEVNULL)
|
||||
subprocess.run(['git','branch','-M','main'],cwd=WORKSPACE,check=True,stdout=subprocess.DEVNULL)
|
||||
env=os.environ.copy(); env.update({'AIOS_GITOPS_ROOT':str(ROOT),'AIOS_GITOPS_WORKSPACE':str(WORKSPACE),'AIOS_GITOPS_DB':str(ROOT/'runtime'/'gitops_model.sqlite3'),'AIOS_GITOPS_DEV_ALLOW':'1','AIOS_GITOPS_HOST':'127.0.0.1','AIOS_GITOPS_PORT':str(PORT)})
|
||||
server=pathlib.Path(__file__).resolve().parents[1]/'active-runtime'/'app'/'aios_gitops_model_api.py'
|
||||
p=subprocess.Popen([sys.executable,str(server)],env=env,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
|
||||
# Wait until the local API is ready; slow CI or busy servers may need a few seconds.
|
||||
def _wait_ready():
|
||||
import urllib.error
|
||||
for _ in range(40):
|
||||
if p.poll() is not None:
|
||||
out = p.stdout.read() if p.stdout else ''
|
||||
raise RuntimeError('server exited early: '+out)
|
||||
try:
|
||||
urllib.request.urlopen(f'http://127.0.0.1:{PORT}/health', timeout=1).read()
|
||||
return
|
||||
except Exception:
|
||||
time.sleep(0.25)
|
||||
out = p.stdout.read() if p.stdout else ''
|
||||
raise RuntimeError('server not ready: '+out)
|
||||
_wait_ready()
|
||||
def call(path,payload=None,user='alice',role='user'):
|
||||
url=f'http://127.0.0.1:{PORT}{path}'; data=None if payload is None else json.dumps(payload).encode(); req=urllib.request.Request(url,data=data,method='POST' if payload is not None else 'GET')
|
||||
if data: req.add_header('Content-Type','application/json')
|
||||
req.add_header('Authorization','Bearer dev'); req.add_header('X-AIOS-User',user); req.add_header('X-AIOS-Role',role)
|
||||
with urllib.request.urlopen(req,timeout=10) as r: return json.loads(r.read().decode())
|
||||
ok=[]
|
||||
try:
|
||||
ok.append(('health',call('/health')['status']=='ok'))
|
||||
add=call('/api/model/custom/add',{'provider_name':'local-test','base_url':'http://127.0.0.1:9999/v1','model_name':'test-model','priority':5},role='user')
|
||||
ok.append(('custom_model_add',add.get('status')=='ok'))
|
||||
pr=call('/api/model/priority/set',{'items':[{'model_id':add['model_id'],'priority':1},'base.deepseekv4-flash']},role='user')
|
||||
ok.append(('priority_set',pr.get('status')=='updated'))
|
||||
route=call('/api/model/route/resolve',{'task_type':'chat'},role='user')
|
||||
ok.append(('route_resolve',route.get('selected',{}).get('id')==add['model_id']))
|
||||
rt=call('/api/runtime/select',{'tier':'regular','risk':'normal'},role='user')
|
||||
ok.append(('runtime_select_regular',rt.get('runtime_type')=='shared'))
|
||||
b=call('/api/branch',{'branch':'patch/uat'},role='admin')
|
||||
ok.append(('branch_create',b.get('status')=='ok'))
|
||||
f=call('/api/file',{'branch':'patch/uat','path':'docs/uat.md','content':'ok','message':'test: uat file'},role='admin')
|
||||
ok.append(('file_commit',f.get('status')=='committed'))
|
||||
finally:
|
||||
p.terminate()
|
||||
try: p.communicate(timeout=5)
|
||||
except Exception: pass
|
||||
failed=[name for name,passed in ok if not passed]
|
||||
print(json.dumps({'status':'PASS' if not failed else 'FAIL','cases':ok,'failed':failed,'root':str(ROOT)},ensure_ascii=False,indent=2))
|
||||
raise SystemExit(0 if not failed else 1)
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import os, sys, time, subprocess, json, urllib.request, urllib.parse, http.cookiejar, socket
|
||||
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
app=root/'active-runtime'/'app'/'aios_webops_app.py'
|
||||
port=int(os.environ.get('AIOS_WEBOPS_UAT_PORT','18189'))
|
||||
env=os.environ.copy()
|
||||
env.setdefault('AIOS_WEBOPS_ROOT', str(root/'active-runtime'))
|
||||
env.setdefault('AIOS_WEBOPS_SECRET','uat-secret')
|
||||
env.setdefault('AIOS_OWNER_USERNAME','owner')
|
||||
env.setdefault('AIOS_OWNER_PASSWORD','bootstrap-change-me')
|
||||
env['AIOS_WEBOPS_PORT']=str(port)
|
||||
env['AIOS_WEBOPS_DB']=str(root/'active-runtime'/'data'/'uat_webops_v5.sqlite3')
|
||||
try:
|
||||
(root/'active-runtime'/'data'/'uat_webops_v5.sqlite3').unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
p=subprocess.Popen([sys.executable,str(app)], env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
base=f'http://127.0.0.1:{port}'
|
||||
try:
|
||||
# wait for ready
|
||||
last=''
|
||||
for _ in range(30):
|
||||
try:
|
||||
with urllib.request.urlopen(base+'/health/ready', timeout=2) as r:
|
||||
ready=json.loads(r.read().decode())
|
||||
break
|
||||
except Exception as e:
|
||||
last=str(e); time.sleep(0.3)
|
||||
else:
|
||||
raise RuntimeError('webops not ready: '+last)
|
||||
assert ready['roles']==['owner','admin','user'], ready
|
||||
assert 'server-hw' in ready['nodes'] and 'server-local' in ready['nodes'], ready
|
||||
# login owner and verify core pages
|
||||
cj=http.cookiejar.CookieJar()
|
||||
opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||
data=urllib.parse.urlencode({'username':'owner','password':'bootstrap-change-me'}).encode()
|
||||
opener.open(urllib.request.Request(base+'/login', data=data, method='POST'), timeout=5).read()
|
||||
for page in ['/dashboard','/nodes','/roles','/routing','/failover','/approvals','/operations','/users','/quota','/models','/memory','/channels/wechat','/life','/patches','/optimization','/simulation','/logs','/config','/uat','/versions','/evidence']:
|
||||
html=opener.open(base+page, timeout=5).read().decode(errors='ignore')
|
||||
assert '<html' in html.lower() or '<!doctype' in html.lower(), page
|
||||
# run UAT through API
|
||||
req=urllib.request.Request(base+'/api/uat/run', data=b'{}', headers={'Content-Type':'application/json'}, method='POST')
|
||||
res=json.loads(opener.open(req, timeout=10).read().decode())
|
||||
assert res.get('status')=='PASS', res
|
||||
print('V5_UAT_OK', json.dumps({'ready':ready,'uat':res}, ensure_ascii=False))
|
||||
finally:
|
||||
p.terminate()
|
||||
try: p.wait(timeout=5)
|
||||
except Exception: p.kill()
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
WEBOPS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
export AIOS_WEBOPS_ROOT="$WEBOPS_ROOT"
|
||||
if [ -f "$WEBOPS_ROOT/config/domains.env" ]; then set -a; . "$WEBOPS_ROOT/config/domains.env"; set +a; fi
|
||||
: "${AIOS_WEBOPS_PORT:=18081}"
|
||||
python3 "$WEBOPS_ROOT/app/aios_webops_app.py"
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
import urllib.request, urllib.parse, json, http.cookiejar, os, sys
|
||||
base=os.environ.get('AIOS_WEBOPS_BASE','http://127.0.0.1:18081')
|
||||
cj=http.cookiejar.CookieJar(); opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||
def form(path,data):
|
||||
req=urllib.request.Request(base+path,data=urllib.parse.urlencode(data).encode(),headers={'Content-Type':'application/x-www-form-urlencoded'},method='POST')
|
||||
return opener.open(req,timeout=10).read().decode()
|
||||
def post(path,data):
|
||||
req=urllib.request.Request(base+path,data=json.dumps(data).encode(),headers={'Content-Type':'application/json'},method='POST')
|
||||
return json.loads(opener.open(req,timeout=10).read().decode())
|
||||
form('/login',{'username':'admin','password':'bootstrap-change-me'})
|
||||
cases=[('text','你好','text'),('quote_text','看一下这个方案','text'),('image','识别图片','vision'),('quote_image','这张图是什么','vision'),('file_pdf','总结一下文件','file'),('file_excel','看一下表格','file'),('quote_file','这个文件有什么问题','file')]
|
||||
bad=[]
|
||||
for typ,msg,expect in cases:
|
||||
r=post('/api/simulation/wechat',{'username':'sample-user','message_type':typ,'content':msg})
|
||||
print(typ, r)
|
||||
if r.get('route_target')!=expect: bad.append((typ,expect,r))
|
||||
if bad:
|
||||
print('WECHAT_SIM_FAIL',bad); sys.exit(2)
|
||||
print('WECHAT_BEHAVIOR_SIMULATION_PASS')
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import urllib.request, urllib.parse, json, http.cookiejar, os
|
||||
base=os.environ.get('AIOS_WEBOPS_BASE','http://127.0.0.1:18081')
|
||||
cj=http.cookiejar.CookieJar(); opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
||||
def form(path,data):
|
||||
req=urllib.request.Request(base+path,data=urllib.parse.urlencode(data).encode(),headers={'Content-Type':'application/x-www-form-urlencoded'},method='POST')
|
||||
return opener.open(req,timeout=10).read().decode()
|
||||
def post(path,data):
|
||||
req=urllib.request.Request(base+path,data=json.dumps(data).encode(),headers={'Content-Type':'application/json'},method='POST')
|
||||
return json.loads(opener.open(req,timeout=10).read().decode())
|
||||
form('/login',{'username':'admin','password':'bootstrap-change-me'})
|
||||
print(json.dumps(post('/api/optimization/diagnose',{}),ensure_ascii=False,indent=2))
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import os, sqlite3, json, time, uuid
|
||||
root=Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]/'active-runtime'))
|
||||
db_path=Path(os.getenv('AIOS_WEBOPS_DB', root/'data'/'webops.sqlite3'))
|
||||
if not db_path.exists():
|
||||
db_path=root/'data'/'webops_v5.sqlite3'
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
con=sqlite3.connect(db_path); con.row_factory=sqlite3.Row
|
||||
now=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
def uid(p): return f'{p}-{int(time.time()*1000)}-{uuid.uuid4().hex[:8]}'
|
||||
findings=[]
|
||||
try:
|
||||
roles=[r['role'] for r in con.execute('SELECT role FROM routing_policy ORDER BY failover_priority')]
|
||||
if roles!=['owner','admin','user']:
|
||||
findings.append(('routing','high','角色迁移顺序异常',str(roles),'修复为 owner/admin/user'))
|
||||
bad=list(con.execute("SELECT username,role FROM users WHERE role NOT IN ('owner','admin','user')"))
|
||||
if bad:
|
||||
findings.append(('security','critical','存在非法角色',json.dumps([dict(x) for x in bad],ensure_ascii=False),'迁移非法角色为 user'))
|
||||
except Exception as e:
|
||||
findings.append(('schema','medium','诊断失败',str(e),'运行迁移后复测'))
|
||||
for cat,sev,title,detail,fix in findings or [('health','low','未发现阻塞问题','静态策略检查通过','无须处理')]:
|
||||
con.execute('INSERT INTO self_optimization_suggestions(id,category,severity,title,detail,proposed_fix,safe_auto_fix,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)',(uid('suggest'),cat,sev,title,detail,fix,1 if sev in ('low','medium') else 0,'open',now))
|
||||
con.commit(); con.close()
|
||||
print(json.dumps({'diagnosis':'ok','findings':len(findings)},ensure_ascii=False))
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess, sys, os
|
||||
print('SELF_OPTIMIZE_START: generating diagnosis suggestions')
|
||||
subprocess.run([sys.executable, os.path.join(os.path.dirname(__file__),'self_diagnose.py')], check=False)
|
||||
print('SELF_OPTIMIZE_DONE: suggestions generated in WebOps /optimization. Owner approval required for risky fixes.')
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import os, sqlite3, time, argparse
|
||||
parser=argparse.ArgumentParser(); parser.add_argument('--safe-only', action='store_true'); args=parser.parse_args()
|
||||
root=Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]/'active-runtime'))
|
||||
db_path=Path(os.getenv('AIOS_WEBOPS_DB', root/'data'/'webops.sqlite3'))
|
||||
if not db_path.exists():
|
||||
db_path=root/'data'/'webops_v5.sqlite3'
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
con=sqlite3.connect(db_path)
|
||||
now=time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
# Safe-only: only restore role migration order and illegal roles. Runtime/code changes still require owner approval.
|
||||
con.execute("UPDATE routing_policy SET failover_priority=1,active_node='server-hw',backup_node='server-local',updated_at=? WHERE role='owner'",(now,))
|
||||
con.execute("UPDATE routing_policy SET failover_priority=2,active_node='server-hw',backup_node='server-local',updated_at=? WHERE role='admin'",(now,))
|
||||
con.execute("UPDATE routing_policy SET failover_priority=3,active_node='server-hw',backup_node='server-local',updated_at=? WHERE role='user'",(now,))
|
||||
con.execute("UPDATE users SET role='user',updated_at=? WHERE role NOT IN ('owner','admin','user')",(now,))
|
||||
con.commit(); con.close()
|
||||
print('SELF_OPTIMIZE_V5_SAFE_FIX_OK')
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
bash "$ROOT/scripts/silent_install.sh"
|
||||
echo "V5_SILENT_INSTALL_OK"
|
||||
echo "WebOps: https://ops.cnnetmkt.top"
|
||||
echo "Digital Life: https://ops.cnnetmkt.top/life"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
RUNTIME_ROOT="${1:-${AIOS_RUNTIME_ROOT:-$(pwd)}}"
|
||||
python3 -m py_compile "$RUNTIME_ROOT/webops/app/aios_webops_app.py"
|
||||
python3 "$RUNTIME_ROOT/webops/app/aios_webops_app.py" >/tmp/aios-webops-smoke.log 2>&1 &
|
||||
PID=$!
|
||||
trap 'kill $PID >/dev/null 2>&1 || true' EXIT
|
||||
sleep 1
|
||||
PORT="${AIOS_WEBOPS_PORT:-18081}"
|
||||
python3 - <<PY
|
||||
import urllib.request, os
|
||||
port=os.environ.get('AIOS_WEBOPS_PORT','18081')
|
||||
resp=urllib.request.urlopen(f'http://127.0.0.1:{port}/health/ready',timeout=5).read().decode()
|
||||
assert 'owner' in resp and 'server-hw' in resp and 'cnnetmkt.top' in resp
|
||||
print('WEBOPS_SMOKE_OK')
|
||||
PY
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys, pathlib, re, os
|
||||
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else os.environ.get('AIOS_RUNTIME_ROOT','.')).resolve(); legacy=re.compile(r'\b(ultra|pro|regular)\b',re.I); viol=[]
|
||||
for base in [runtime/'webops']:
|
||||
if base.exists():
|
||||
for p in base.rglob('*'):
|
||||
if p.is_file() and p.suffix.lower() in ['.py','.sh','.yaml','.yml','.json','.env','.md','.html','.js','.sql']:
|
||||
s=p.read_text(errors='ignore')
|
||||
if 'legacy_role_mapping' in s: continue
|
||||
if legacy.search(s): viol.append(str(p))
|
||||
if viol:
|
||||
print('LEGACY_ROLE_REFERENCES_FOUND'); print('\n'.join(viol[:20])); raise SystemExit(2)
|
||||
print('NO_LEGACY_ROLES_IN_ACTIVE_OVERLAY_OK')
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys, pathlib, os
|
||||
runtime=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else os.environ.get('AIOS_RUNTIME_ROOT','.')).resolve()
|
||||
candidates=[
|
||||
runtime/'webops/config/domains.env',
|
||||
runtime/'webops/config/roles.yaml',
|
||||
runtime/'AIOS_ACTIVE_ROLE_MODEL_OWNER_ADMIN_USER',
|
||||
runtime/'config/domains.env',
|
||||
runtime/'config/roles.yaml',
|
||||
runtime/'config/nodes.yaml',
|
||||
runtime/'config/routing.yaml',
|
||||
]
|
||||
text=''
|
||||
for p in candidates:
|
||||
if p.exists():
|
||||
text += p.read_text(errors='ignore')+'\n'
|
||||
required=['owner','admin','user','server-hw','server-local','https://cnnetmkt.top','https://ops.cnnetmkt.top']
|
||||
missing=[x for x in required if x not in text]
|
||||
if missing:
|
||||
raise SystemExit('VALIDATE_ROLES_DOMAINS_FAIL '+str(missing))
|
||||
print('VALIDATE_ROLES_DOMAINS_OK')
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"source_zip": "aios_v23_RUNTIME_FULL_DEPLOY_20260706T113802_ATTACHMENT_ROUTE_GUARD_FULL.zip",
|
||||
"source_file_count": 1168,
|
||||
"active_role_model": [
|
||||
"owner",
|
||||
"admin",
|
||||
"user"
|
||||
],
|
||||
"logical_nodes": [
|
||||
"server-hw",
|
||||
"server-local"
|
||||
],
|
||||
"domains": {
|
||||
"public_user_domain": "https://cnnetmkt.top",
|
||||
"webops_domain": "https://ops.cnnetmkt.top",
|
||||
"api_domain": "https://api.cnnetmkt.top",
|
||||
"wechat_callback_domain": "https://wechat.cnnetmkt.top"
|
||||
},
|
||||
"purpose": "delta-only WebOps full refresh on top of latest actual deployment package"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"current_actual": {
|
||||
"name": "aios_v23_RUNTIME_FULL_DEPLOY_20260706T113802_ATTACHMENT_ROUTE_GUARD_FULL.zip",
|
||||
"size": 5237501,
|
||||
"sha256": "8456cf86202ef46f9ca6e61b14f1763f298755e5eb36d42051306620c49f471c",
|
||||
"files": 1168,
|
||||
"docs": 513,
|
||||
"scripts": 324,
|
||||
"configs": 112,
|
||||
"tests": 189
|
||||
},
|
||||
"base_delta": {
|
||||
"name": "AIOS_v23_webops_full_refresh_PROD_DELTA_20260706.zip",
|
||||
"size": 4898201,
|
||||
"sha256": "0ebedbf51b658f4d98fa09e1e6169c48e4396ac33ebab0088a351de313501b73",
|
||||
"files": 44,
|
||||
"docs": 11,
|
||||
"scripts": 22,
|
||||
"configs": 9,
|
||||
"tests": 2
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
PKG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
: "${AIOS_RUNTIME_ROOT:=$PKG_ROOT/.test_runtime}"
|
||||
rm -rf "$AIOS_RUNTIME_ROOT"; mkdir -p "$AIOS_RUNTIME_ROOT"
|
||||
bash "$PKG_ROOT/scripts/install_webops_overlay.sh" "$AIOS_RUNTIME_ROOT"
|
||||
python3 "$PKG_ROOT/scripts/apply_migrations.py" "$AIOS_RUNTIME_ROOT"
|
||||
for s in run_full_uat.py run_wechat_behavior_simulation.py self_diagnose.py self_optimize.py run_acceptance_with_autofix.sh; do cp "$PKG_ROOT/scripts/$s" "$AIOS_RUNTIME_ROOT/webops/scripts/$s"; chmod +x "$AIOS_RUNTIME_ROOT/webops/scripts/$s"; done
|
||||
bash "$AIOS_RUNTIME_ROOT/webops/scripts/run_webops_local.sh" >/tmp/aios-webops-pkg-uat.log 2>&1 &
|
||||
PID=$!
|
||||
trap 'kill $PID >/dev/null 2>&1 || true' EXIT
|
||||
sleep 1
|
||||
AIOS_WEBOPS_BASE="http://127.0.0.1:${AIOS_WEBOPS_PORT:-18081}" bash "$AIOS_RUNTIME_ROOT/webops/scripts/run_acceptance_with_autofix.sh"
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
python3 scripts/run_v1r1c0_uat.py
|
||||
bash scripts/package_contract_check_v1r1c0.sh
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
python3 "$ROOT/scripts/validate_roles_and_domains.py"
|
||||
python3 "$ROOT/scripts/validate_no_legacy_roles.py"
|
||||
if ! python3 "$ROOT/scripts/run_v5_uat.py"; then
|
||||
echo "V5 UAT failed; running self-diagnosis and safe rollback-ready repair plan" >&2
|
||||
AIOS_WEBOPS_ROOT="$ROOT/active-runtime" python3 "$ROOT/active-runtime/scripts/apply_migrations.py" || true
|
||||
AIOS_WEBOPS_ROOT="$ROOT/active-runtime" python3 "$ROOT/scripts/self_diagnose_v5.py" || true
|
||||
AIOS_WEBOPS_ROOT="$ROOT/active-runtime" python3 "$ROOT/scripts/self_optimize_v5.py" --safe-only || true
|
||||
python3 "$ROOT/scripts/run_v5_uat.py"
|
||||
fi
|
||||
echo "V5_ACCEPTANCE_WITH_AUTOFIX_OK"
|
||||
@@ -0,0 +1,4 @@
|
||||
from pathlib import Path
|
||||
|
||||
def test_api_file_exists():
|
||||
assert (Path(__file__).resolve().parents[1] / 'active-runtime/app/aios_gitops_model_api.py').exists()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user