yumoqing 93079c1a54 refactor(skills): 精炼 7 个开发技能文本(压缩 WRONG/CORRECT 双代码块为规则+警示,规则100%保留)
module-development-spec 99KB→57KB、harnessed-module-development 101KB→62KB、
crud-definition-spec 78KB→46KB、dspy-file-implementation-spec 51KB→26KB、
ahserver 50KB→26KB、sqlor-database-module 49KB→26KB、database-table-definition-spec 20KB→19KB
合计 448KB→263KB,压缩 41%,节省约 92k token
2026-08-21 12:24:26 +08:00

26 KiB
Raw Blame History

name description version author tags dependencies
ahserver ahserver - Asynchronous HTTP(S) web application framework based on aiohttp 1.3.0 yu moqing
aiohttp
web-framework
async
python
http-server
uvloop
httptools
redis
asyncio
aiofiles
aiodns
aiohttp==3.10.10
aiohttp_session
aiohttp_auth_autz
aiohttp-middlewares
aiohttp-cors
aiomysql
aioredis
psycopg2-binary
aiopg
jinja2
ujson
openpyxl
pillow
py-natpmp

ahserver Web Application Framework

Overview

Async HTTP(S) server built on aiohttp. Features: auth/authorization, HTTPS (SSL/TLS), multi-DB connection pools (MySQL/PostgreSQL/Oracle/SQL Server), processors for .dspy/.tmpl/.md/.xlsxds/.sqlds/.wss, i18n, auto-stored file uploads, background tasks, RESTful APIs.

Installation

  • Python 3.8+. From source: cd /tmp/ahserver && pip install -e .
  • Python 3.12+ aioredis fix: pip install packaging, then edit aioredis/connection.py:11from packaging.version import Version as StrictVersion; aioredis/exceptions.py:14class TimeoutError(asyncio.TimeoutError, RedisError):

Project Structure

your-app/
├── ah.py               # entry point
├── conf/config.json
├── i18n/
└── app/
    ├── index.html
    ├── api/status/index.dspy   # → /api/status (directory pattern, 1.2.0+)
    ├── template.tmpl           # Jinja2
    └── data.sqlds

⚠️ 1.2.0+ requires directory/index.dspy for APIs — flat api/status.dspy no longer resolves to /api/status. See references/api-routing-patterns.md.

Configuration (conf/config.json)

Top-level keys: password_key, databases, website, langMapping.

  • website: paths: [["$[workdir]$/app", ""]], host: "0.0.0.0", port: 8080, coding: "utf-8", optional ssl: {crtfile, keyfile}, indexes: ["index.html","index.ui","index.tmpl","index.dspy","index.md"], processors: [[".xlsxds","xlsxds"],[".sqlds","sqlds"],[".tmpl","tmpl"],[".dspy","dspy"],[".md","md"]].
  • langMapping: {"zh-Hans-CN": "zh-cn", "en-US": "en"}.

⚠️ password_key must be a TOP-LEVEL field (same level as databases). sqlor reads it via getConfig().password_key to AES-decrypt DB passwords. Without it, self.unpassword() returns None and connection fails.

Database entries (all use coding: "utf8" + dbname; kwargs differ by driver)

Driver kwargs
MySQL/MariaDB: mysql.connector user, db, password, host
PostgreSQL: psycopg2 database, user, password, host, port
Oracle: cx_Oracle user, host, dsn (e.g. 10.0.185.137:1521/SAMPLEDB)
SQL Server: pymssql user, database, password, server, port (1433), charset (utf8)

Passwords are AES-ECB base64 (see Password Encryption below).

Usage Examples

Basic Application Setup (ah.py) — startup hooks

Version API Import
≤1.0.x RegisterCoroutine().register('ahapp_built', cb) ahserver.configuredServer
≥1.2.0 add_startup(cb) ahserver.configuredServer

⚠️ RegisterCoroutine REMOVED in 1.2.0 → use add_startup. Callback signature changed: old (app) → new (*args, **kw).

from ahserver.webapp import webapp
from ahserver.serverenv import ServerEnv
from ahserver.configuredServer import add_startup

async def on_app_built(*args, **kw):        # runs after auth middleware + processors are set up
    asyncio.ensure_future(my_background_task())

def init():
    env = ServerEnv()
    env.get_module_dbname = lambda m: 'your_db_name'
    add_startup(on_app_built)

if __name__ == '__main__':
    webapp(init)

Authentication API

from ahserver.auth_api import AuthAPI
class MyAuthAPI(AuthAPI):
    def needAuth(self, path): ...                    # True → require auth
    async def getPermissionNeed(self, path): ...     # permission name for path
    async def checkUserPassword(self, user_id, password): ...
    async def getUserPermissions(self, user): ...    # list of permission names
# run: ConfiguredServer(MyAuthAPI).run()

Processor Types

.dspy (Dynamic Python Scripts)

Execution model: code runs directly in script scope; the framework captures the script's TOP-LEVEL return as the response.

⚠️ Setting a result variable alone does NOT work (framework sees NoneType). Do NOT wrap code in async def run() — its return goes nowhere at script scope. Always return at top level:

return json.dumps({'status': 'success', 'data': result}, ensure_ascii=False)

In-scope helpers: await get_user(), await redirect('/login'), uuid(), DBPools(); CRUD via async with db.sqlorContext('dbname') as sor:await sor.C('tbl', ns), sor.R('tbl', ns), sor.U('tbl', ns), sor.D('tbl', {'id':...}), sor.sqlExe("SELECT ... WHERE id=${id}$", {'id':...}) (placeholders ${name}$), sor.sqlPaging(sql, {'search':..., 'page':1, 'pagerows':20, 'sort':'name'}).

File Upload

Multipart uploads auto-handled: files are saved by FileStorage BEFORE the .dspy runs; params_kw contains the relative web_path, not the file object.

from ahserver.filestorage import FileStorage
abs_path = FileStorage().realPath(params_kw.get('audio_file'))  # '/66/34/59/64/file.mp3' → '/tmp/66/34/59/64/file.mp3'

See references/file-upload-patterns.md (examples, curl testing, debugging).

.tmpl / .sqlds / .xlsxds / .md

  • .tmpl: Jinja2 template rendered with request context (see Jinja2 filter pitfall below).
  • .sqlds: SQL query exposed as data source; .xlsxds: Excel as structured data; .md: rendered as HTML.

.wss (WebSocket Endpoints)

Must contain async def myfunc(request, **kwargs). Behavior (websocketProcessor.py): reads client Sec-WebSocket-Protocol header as cookie → get_user() auth; injects ws_pool (WsPool) and ws_data (message text) into kwargs; runs myfunc per incoming TEXT message; ws_pool.sendto(data, id=None) pushes JSON.

async def myfunc(request, **kwargs):
    data = json.loads(kwargs.get('ws_data') or '{}')
    if data.get('cmd') == 'ping':
        await kwargs['ws_pool'].sendto(json.dumps({'type': 'pong'}))

⚠️ Frontend must pass the cookie: new WebSocket(url, document.cookie) — without it auth fails.

Global Environment Variables

  • Session (async): get_user(), remember_user(userid, username='', userorgid=''), forget_user(), redirect(url), entire_url(url) (full URL with scheme/host/port), gethost() (client IP), path_call(path, **kw) (call other server resources).
  • Globals: configValue(k) (e.g. configValue('.website.port')), DBPools() (sqlor pools), uuid(), curDatetime(), str2date(dstr), str2datetime(dstr), server_error(errcode) (400/401/403/404/500...).
  • Built-in modules: time, datetime, random, json, ArgsConvert, DictObject.

CRUD with SQLor

All CRUD requires a table with id primary key. Pattern:

db = DBPools()
async with db.sqlorContext('dbname') as sor:
    await sor.C('tbl', {'id': uuid(), 'field1': 'value1'})   # create
    recs = await sor.R('tbl', params_kw.copy())              # read (client params)
    await sor.U('tbl', params_kw.copy())                     # update
    await sor.D('tbl', {'id': params_kw.id})                 # delete
    recs = await sor.RP('tbl', ns)   # paged → {'total': n, 'rows': [...]}; ns: page/sort/pagerows

Running Behind Nginx

Forward these headers:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Url $request_uri;
proxy_set_header X-Forwarded-Prepath "";

Performance

Middleware chain per request: real_ip_middleware → session_middleware → auth_api.checkAuth() → ProcessorResource._handle() → handler.

Per-request overhead (local SSD): Redis session GET 0.06ms (only if session_redis); info() log flush 0.01ms (production NFS/cloud: 10100ms); _handle() closures + isHtml 0.22ms; network + file I/O 1.23ms. ⚠️ Production latency can be 1050x higher (NFS/cloud disk flush, I/O contention, multiple log calls per request).

  • Log flush (P0+P3 FIXED, commits 574ef00/5238a08, appPublic/log.py): file opened once + kept open; threading.Queue(maxsize=10000) + daemon thread for non-blocking writes; only exception/critical flush immediately, others every ~1s idle; queue-full drops oldest instead of blocking the loop; public API (info() etc.) unchanged. ⚠️ On local SSD flush ≈ 0.01ms — if prod disk is local SSD, log flush is NOT the bottleneck.
  • isHtml() (FIXED, processorResource.py:398): now reads only first 512 bytes (f.read(512) + decode('utf-8', errors='ignore')) instead of the whole file (was reading e.g. 48KB bricks.js / 1MB echarts.min.js).
  • Static file fast path (APPLIED, 574ef00): at very top of ProcessorResource._handle() (before parse_request/closures/i18n/isHtml): if request.path.lower() ends with a static ext (.js .css .png .jpg .jpeg .gif .ico .svg .woff .woff2 .ttf .eot .map .webp .bmp .mp3 .mp4 .webm .ogg .wav) → self.parse_request(request) then return await super()._handle(request). ⚠️ parse_request MUST run first — super() needs self._preurl set by it.
  • Session on static files: with session_redis, every request incl. static triggers a Redis GET. Consider EncryptedCookieStorage instead of RedisStorage for static-heavy apps, or bypass session for known static paths.

timecost log format (auth_api.py)

timecost=client(IP) user_id access /path cost TOTAL, (AUTH_MS) — TOTAL = full handler execution; AUTH_MS = auth/permission check only. Diagnostic rule: AUTH_MS small (13ms) but total slow → bottleneck is in the handler / .dspy, NOT RBAC.

Unauthenticated static files

/bricks/3parties/, /bricks/css/, /bricks/*.js require any role permission in load_path.py; otherwise unauthenticated requests return need login (redirect, not 403). Check rbac/check_perm.py logs: userid=None, path='...' permission check failed,userroles=['anonymous', 'any'].

Pitfalls

⚠️ CRITICAL: StreamResponse breaks aiohttp_auth ticket reissue — active users get kicked out

aiohttp_auth.process_response() renews tickets only for web.Response instances; ahserver returns StreamResponse, so the check always fails and tickets are never renewed — users logged out after session_max_time (default 2h) regardless of activity. Fix in auth_api.py checkAuth after ret = await handler(request):

from aiohttp_auth.auth.ticket_auth import _REISSUE_KEY
if _REISSUE_KEY in request:
    policy = request.get('aiohttp_auth.policy')
    if policy and hasattr(policy, 'remember_ticket'):
        await policy.remember_ticket(request, request[_REISSUE_KEY])

Diagnostic: users "kicked out while actively using the app" → verify this fix is present. See references/session-timeout-architecture.md.

⚠️ 1.2.0+ routing: use directory/index.dspy OR apply extensionless-URL fallback patch

≤1.0.x auto-resolved /api/status/api/status.dspy; 1.2.0 removed auto-extension → extensionless URLs raise Exception: ... invalid path (HTTP 500). bricks/i18n.js hardcodes /i18n_getmsgs (no extension) → every Bricks app 500s on load without a fix.

Recommended: directory pattern (api/status/index.dspy, resolved natively via indexes config). Migration: cd app/api && for f in *.dspy; do mkdir -p ${f%.dspy} && mv $f ${f%.dspy}/index.dspy; done.

Fallback patch (commit 7a297e9, 2026-06-25) — only for hardcoded extensionless URLs:

  1. processorResource.py _handle(): when url2file(path) returns None, try url2file(path + ext) for each registered processor extension; then after url2processor, if request_filename is set and processor has no real_path, set processor.real_path = self.request_filename.
  2. baseProcessor.py set_run_env(): compute real_path only if not already set (plain url2file(request.path) returns None for extensionless URLs). Why two patches: BaseProcessor.__init__ doesn't set real_path; patch 1 sets it before handle(), patch 2 stops set_run_env overwriting it. Verify: curl -u admin:pass http://localhost:9090/i18n_getmsgs → HTTP 200 {"success": true, "msgs": {...}}; if 500 with log TypeError: expected str, bytes or os.PathLike object, not NoneType, patch 2 is missing. Not recommended: nginx rewrite (rewrite ^/api/(.*)$ /api/$1.dspy break;) — adds complexity, bypasses framework resolution.

⚠️ aligner service: own venv + numpy<2 + PYTHONPATH

Aligner (/data/ymq/aligner/) uses its own venv /data/ymq/aligner/py3/ (NOT /share/vllm-0.8.5); requires ctc_segmentation and numpy<2 (numpy>=2 breaks ctc_segmentation). Start with PYTHONPATH=/data/ymq/aligner/app:/data/ymq/aligneraligner.py does from align import AlignEngine; without PYTHONPATH: ModuleNotFoundError: No module named 'align'. Listens on 8080, POST /api/align {audio_path, text}.

⚠️ demucs pitfalls

  • /share/vllm-0.8.5 venv is root-owned → sudo /share/vllm-0.8.5/bin/pip install demucs; binary /share/vllm-0.8.5/bin/demucs; use -n htdemucs (4-stem) or -n htdemucs_ft (fine-tuned, better but slower).
  • demucs 4.0.1 + newer torchaudio: torchaudio.save()save_with_torchcodec() fails RuntimeError: Could not load libtorchcodec (needs libnvrtc.so.13, CUDA runtime, not on shared lib path). Separation completes 100% but save crashes. Fix: wrapper script monkey-patching torchaudio.save to soundfile (sf.write(uri, wav.T if wav.shape[0]<=wav.shape[1] else wav, sample_rate)); requires soundfile (present in /share/vllm-0.8.5); invoke: /share/vllm-0.8.5/bin/python /tmp/demucs_wrapper.py --two-stems=vocals -o <out> <in>.

⚠️ GPU services: independent venv + longtasks pattern, NEVER FastAPI

Rule: all GPU services on the media server (ymq@opencomputing.net) MUST use ahserver + longtasks, not FastAPI. Each service needs its OWN venv — never install into /share/vllm-0.8.5 (diffusers 0.35.2 + old transformers → HybridCache import error). CRITICAL: search existing code FIRST (media-server, aligner, songrate; session_search(query="longtasks ahserver deploy")) — reuse proven implementations, don't reinvent. longtasks quirks: submit_task(payload) returns a dict {'task_id': '...'} (not a string) → task_id = result.get('task_id'); get_status(task_id)PENDING|RUNNING|SUCCEEDED|FAILED. Template (ah.py): subclass LongTasks.process_task(payload, workid=None) (payload may be JSON string — json.loads it); in on_app_built: if env.longtasks: schedule_once(0.1, env.longtasks.run); in init(): env.longtasks = MyTasks('redis://127.0.0.1:6379', 'myqueue', worker_cnt=1, stuck_seconds=3600). Setup checklist: python3 -m venv ~/my-service/venvpip install ahserver appPublic sqlor longtasks aiohttp → write ah.py + .dspy routes → conf/config.json (port, paths, processors) → nohup python ah.py > service.log 2>&1 &. GitHub is blocked on the GPU server: SOCKS5 tunnel ssh -N -D 1086 ymq@proxy-server → clone with git -c http.proxy=socks5h://127.0.0.1:1086 -c https.proxy=socks5h://127.0.0.1:1086 clone <url>scp -r to server. See references/gpu-service-longtasks-pattern.md (wan22 example — DEPRECATED, replaced by wan27); references/ssh-access-map.md (passwordless hosts, server inventory).

⚠️ startswiths config: must use registerfunction, NOT path

{"leading": "/api/foo", "path": ...}KeyError: 'registerfunction' on EVERY request (FunctionProcessor.path_call() unconditionally reads config_opts['registerfunction']; path belongs to a different routing mechanism). Correct:

"startswiths": [{"leading": "/idfile", "registerfunction": "idfile"}]

Prefer directory/index.dspy for new APIs; use startswiths + registerfunction only for Python-registered function handlers (RegisterFunction).

⚠️ idfile download endpoint: explicit import AND config required

/idfile?path=... → 500 "invalid path" unless BOTH: (1) from ahserver import filedownload in ah.py (registers idfile/download via RegisterFunction at import time; path_download() resolves path via FileStorage().realPath() → file_response); (2) startswiths entry {"leading": "/idfile", "registerfunction": "idfile"}. ⚠️ Chinese chars in path query MUST be URL-encoded (%E9%94%99%E9%A2%91, not 错频) — server rejects unencoded.

⚠️ appPublic.worker missing schedule functions (legacy ≤5.2.x / ahserver ≤1.0.8)

Only exports AsyncWorkerfrom appPublic.worker import schedule_once → ImportError. Fixed in appPublic ≥5.3.0 (pip install --upgrade apppublic). Workaround: define local get_event_loop() (= asyncio.get_event_loop()), schedule_once(delay, coro_func) and schedule_interval(interval, coro_func) as asyncio.ensure_future wrappers. Applies to longtasks and any appPublic.worker scheduling code; clear __pycache__ after patching.

⚠️ stream_response re-raises ClientConnectionResetError — noisy logs

ahserver/globalEnv.py stream_response() wraps ALL write errors in a generic Exception and re-raises, including ClientConnectionResetError (client disconnected mid-stream) → every client timeout during SSE floods logs with stack traces. Fix (commit 387726e): catch ClientConnectionResetError separately and break the streaming loop; other exceptions still logged (write error{e=}, {d=}) and re-raised.

⚠️ @routes decorator removed in 1.2.0+ — use app.router.add_route()

from ahserver.webapp import webapp, routes, add_startupImportError: cannot import name 'routes'; leftover @routes.get(...)NameError. Fix: register inside on_app_built:

async def on_app_built(app):
    app.router.add_route('GET', '/api/health', health)

Migration: grep -rn '@routes\.' /data/ymq/*/ah.py, remove decorator lines, register handlers in on_app_built.

⚠️ CRITICAL: @ inside comments can truncate files on production deploy

Incident 2026-05-26: auth_api.py comment # redis = await aioredis.from_url("redis://127.0.0.1:6379") preceded by @web.middleware text — the deployment process truncated the file at that line, losing ~30 lines (aiohttp_session.setup(app, storage), auth.setup(app, policy), app.middlewares.append(self.checkAuth)). Symptom: all requests 500, checkAuth never fires, no auth logs. Prevention: never put @something patterns in comments (esp. strings containing @); reference URLs without the @user:pass portion. After deploying auth_api.py changes ALWAYS verify: grep -c "middlewares.append" auth_api.py ≥ 1; grep -E "middlewares.append|setup\(app|aiohttp_session.setup" auth_api.py → 3+ lines.

⚠️ Jinja2 in .ui/.tmpl: limited filter set

Non-standard filters (e.g. |ternary) → jinja2.exceptions.TemplateAssertionError: No filter named 'ternary'. Only standard Jinja2 filters + ahserver built-ins (entire_url(), get_user(), configValue()) are registered. Use if/else: {{get_user().nick_name if get_user() else '登录'}}. Rule: keep Jinja2 expressions simple in .ui JSON templates; complex conditionals handled client-side in JS.

⚠️ Never modify sage/conf/config.json during feature development

It is the PRODUCTION config for the Sage platform. Config changes (DB password, session settings) are handled manually by the user.

Verify auth_api.py after any edit

wc -l ahserver/auth_api.py ≈ 193 (not fewer); grep "middlewares.append" present; python3 -c "import ast; ast.parse(open('ahserver/auth_api.py').read())" parses.

Common Issues

Password Encryption

DB passwords in conf/config.json are AES-ECB base64 using password_key:

from appPublic.aes import aes_encode_b64
encoded = aes_encode_b64(getConfig().password_key, 'plaintext_password')

sqlor/sor.py calls self.unpassword() in __init__ (aes_decode_b64). ⚠️ Password MUST be valid AES-ECB base64 — plain text or RC4-encoded → ValueError: The length of the provided data is not a multiple of the block length. Quick encode: python3 -c "from appPublic.aes import aes_encode_b64; print(aes_encode_b64('YOUR_PASSWORD_KEY', 'test'))". Legacy RC4: python -m ahserver.dbpassword /path/to/app password123.

Debugging

Check console logs; format_exc() for detailed errors; request context via request._run_ns.

Performance: benchmark conclusions

May 2026 (local SSD, Redis session storage, clientinfo level, 48KB static file): pure aiohttp 1.23ms vs ahserver before fixes 1.13ms / after 3 fixes 1.10ms. → ahserver code overhead is negligible; a 17s production delay is NOT caused by ahserver. Likely causes: network/proxy layer (nginx reverse proxy, TLS handshake, keep-alive), DNS, connection pool exhaustion, deployment differences. Diagnostics: (1) browser Network panel TTFB; (2) timing middleware (@web.middleware logging time.time() around handler); (3) compare with/without nginx in path; (4) timecost AUTH_MS rule above. See references/performance-benchmark-may2026.md, references/performance-diagnostics.md.

Multi-Service Deployment

Services are capability centers, not agents. Each service: exposes atomic capabilities via REST, accepts uploads/paths as input, returns results (paths/URLs/status), knows nothing about upstream/downstream steps. The orchestrating agent: calls services in sequence, handles state transitions/error recovery/retry, and is the file router — downloads from one service, uploads to the next. Services must NOT copy files directly between each other, even co-located (keeps pipelines distributable). See references/multi-service-media-platform.md (geo-block IP filtering, async longtasks vs sync lock split, port conventions, inter-service localhost calls); references/nginx-proxy-ssl-template.md (SSL non-standard port, X-Forwarded headers, Let's Encrypt DNS-01 where 80/443 unavailable).

Two-layer session system (aiohttp_session + aiohttp_auth ticket) — timeout/renewal semantics and sliding-window ticket renewal: see references/session-timeout-architecture.md.

Hot Reload

Built-in (ahserver/hotreload.py): watches file mtimes via stat(), multi-process safe with reuse_port=True (each worker runs its own FileWatcher; no Redis pub/sub, signals, or cross-process coordination).

  • Auto (no config): .dspy/.md read from disk every request (no cache); .tmpl/.ui via Jinja2 auto_reload (mtime check).
  • Needs hot_reload config: conf/config.json (JsonConfig singleton; cleared on change, next getConfig() reloads) and i18n/*/msg.txt (MiniI18N singleton + ServerEnv.myi18n; both cleared).
"hot_reload": true                              // enable, default 2s interval
"hot_reload": {"enabled": true, "interval": 5}  // custom interval
// omit or false → disabled (default)
  • GET /__hot_reload__ (only when enabled) triggers invalidate_all_caches(): rbac (UserPermissions.ur_caches + rp_caches → LRU.clear() + invalidate_rp_cache()), pricing (PricingProgram.pricing_data → clear()), uapi (UAPIData.apidata + org_users → clear()), llmage (_uapi_cache + _uapiio_cache → invalidate). Each module cleared independently (try/except — one failure doesn't block others).
  • Cache TTLs: rbac.ur_caches 5min, rbac.rp_caches 10min (DB events), pricing.pricing_data none (DB events), llmage._uapi_cache 5min (invalidate_uapi_cache()), uapi.UAPIData: NO TTL, NO auto-invalidation — use /__hot_reload__. ⚠️ Multi-process caveat: with reuse_port=True /__hot_reload__ only clears the single worker handling the request — hit multiple times or rely on the file-mtime watcher (works in all workers independently).

Version History

  • 1.3.0: hot-reload module, file watching, /__hot_reload__ endpoint, invalidate_all_caches().
  • 1.2.0 (BREAKING): RegisterCoroutine REMOVED → add_startup(callback); .dspy auto-extension REMOVED (/api/status no longer → /api/status.dspy; needs directory/index.dspy, startswiths, or the fallback patch); added uvloop + httptools; appPublic.worker now exports schedule_once/schedule_interval/get_event_loop (was only AsyncWorker in ≤5.2.x).
  • 1.0.8: added server_error(errcode) global; request._run_ns for global env access.

References

  • references/api-routing-patterns.md — directory/index.dspy, nginx proxy_pass trailing slash semantics, startswiths vs processor lookup, 1.0.x → 1.2.0 migration cheat sheet.
  • references/api-testing-patterns.md — test ahserver APIs with Python urllib instead of curl (avoids shell escaping issues with Bearer tokens / JSON payloads).
  • references/file-upload-patterns.md — upload examples, curl testing, debugging.
  • references/session-timeout-architecture.md — two-layer session, timeout/renewal.
  • references/performance-benchmark-may2026.md, references/performance-diagnostics.md — benchmark results, middleware chain breakdown.
  • references/multi-service-media-platform.md, references/nginx-proxy-ssl-template.md, references/ssh-access-map.md — multi-instance nginx deployment, SSL template, SSH access map.
  • references/vllm-integration-pattern.md — AsyncLLMEngine + stream_response for high-throughput inference; 8 GPUs × 2 instances; critical params --max-model-len/--max-num-seqs/--gpu-memory-utilization; pitfalls: heredoc variable expansion, ClientConnectionResetError, dead DB connections after OOM; transformers → vLLM migration.
  • references/gpu-service-longtasks-pattern.md (wan22 example — DEPRECATED, replaced by wan27); references/wan22-deployment.md (DEPRECATED).