Kaynağa Gözat

restore: README / legacy packages / index scripts dropped by previous push; drop _host; index rebuilt with full version history

王硕 4 gün önce
ebeveyn
işleme
117c94f092

+ 142 - 0
README.md

@@ -0,0 +1,142 @@
+# TeamPLT Gallery
+
+TeamPLT / TeamAAS 2.1 平台的扩展包仓库。
+
+## TeamAAS 2.1 是什么
+
+TeamAAS 2.1 是一套面向 3C 电子制造、精密组装、检测分拣场景的工业自动化控制与机器视觉软件。它把相机、机械手、运动控制卡、光源控制器、震动盘、PLC 通讯等硬件整合到一个主控进程里,通过流程图(Flow)编排生产工序:
+
+- **硬件层**:统一管理相机(海康/巴斯勒等)、机械手(Epson/雅马哈/国产)、运动控制卡、光源、震动盘、PLC(Modbus/TCP/Serial)
+- **视觉层**:集成多种视觉算法引擎(定位、缺陷、测量、OCR)
+- **流程层**:拖拽式流程图编排工序,节点连线即逻辑,支持单步调试、监控运行
+- **生产层**:产品配方管理、生产计数、OK/NG 统计、良率报表
+
+TeamPLT 是它的微内核外壳,负责插件加载、界面框架、扩展管理。本仓库(Gallery)就是给这个微内核喂扩展包的源。
+
+## Gallery 是什么
+
+Gallery 是 TeamPLT 的扩展包源(package source)。平台内置"扩展管理"页,配置本地址后可以浏览、下载、安装扩展,下载后按 sha256 强校验。
+
+仓库结构:
+
+```
+index.json                                    # 包索引(平台只认这个文件)
+regen-index.ps1                               # 自动生成 index.json 的脚本
+packages\<host>\<id>\<id>-<version>.zip       # 扩展包
+```
+
+`host` 是扩展挂载位置:
+
+| host | 说明 |
+|------|------|
+| `platform` | 平台级扩展(扩展主界面、工具条、AI 等) |
+| `vision` | 视觉算法插件 |
+| `cameras` | 相机驱动子插件 |
+| `communication` | 通讯/PLC 驱动子插件 |
+| `base` | 基础库/共享组件 |
+
+## 配置为平台包源
+
+编辑平台 `Config\gallery-sources.json`,加一条:
+
+```json
+{
+  "id": "teamplt",
+  "name": "TeamPLT Gallery",
+  "location": "http://49.235.130.76/TeamPLT/Gallery/raw/master/index.json",
+  "enabled": true
+}
+```
+
+重启平台 → 扩展管理页即可看到包列表,点安装自动下载、校验、加载。
+
+## 如何提交自己的扩展
+
+### 1. 编译扩展
+
+扩展工程引用 `TeamPLT.SDK.dll` 和 `TeamPLT.Contracts.dll`,编译产出目录(含 `manifest.json`、dll、本地化文件)整体就是一个扩展。
+
+### 2. 打 zip
+
+把产出目录的内容**直接压成 zip**(不要多套一层文件夹)。zip 根目录应该长这样:
+
+```
+manifest.json
+Ext.XXX.dll
+Localization\zh-CN.json
+```
+
+### 3. 放到仓库
+
+路径规则:`packages\<host>\<id>\<id>-<version>.zip`
+
+例如:`packages/platform/Ext.AiAssistant/Ext.AiAssistant-1.0.0.zip`
+
+### 4. 更新 index.json(推荐用脚本)
+
+把 zip 放进 `packages/` 对应目录后,在仓库根目录跑:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File .\regen-index.ps1
+```
+
+脚本会自动扫描所有 zip、读包内 manifest.json、算 sha256/size,生成与平台 `GalleryPackage` 类对齐的 index.json(含多版本 `versions[]`)。旧 index 里的版本历史会自动继承。
+
+也可以手动编辑 index.json,字段格式如下:
+
+```json
+{
+  "id": "Ext.YourExt",
+  "name": "扩展显示名",
+  "version": "1.0.0",
+  "publisher": "TeamAAS",
+  "kind": "extension",
+  "host": "platform",
+  "description": "一句话说明扩展做什么",
+  "notes": "• 功能点一\n• 功能点二",
+  "downloadUrl": "packages/platform/Ext.YourExt/Ext.YourExt-1.0.0.zip",
+  "sha256": "zip 文件的 sha256",
+  "size": 123456,
+  "references": [ "TeamPLT.SDK", "TeamPLT.Contracts" ],
+  "targetFramework": "net48",
+  "requiresHost": "TeamPLT>=2.1.0",
+  "arch": [ "x64" ],
+  "dependencies": [],
+  "versions": [
+    {
+      "version": "1.0.0",
+      "downloadUrl": "packages/platform/Ext.YourExt/Ext.YourExt-1.0.0.zip",
+      "sha256": "zip 文件的 sha256",
+      "size": 123456,
+      "releasedAt": "2026-09-24",
+      "status": "stable",
+      "notes": "首个版本"
+    }
+  ]
+}
+```
+
+发新版本时:顶层 `version`/`downloadUrl`/`sha256`/`size` 更新到最新版,`versions` 数组追加一条。`status` 可选 `stable` / `beta` / `deprecated`。
+
+### 5. 提交推送
+
+```bash
+git add -A
+git commit -m "Gallery: 收录 Ext.YourExt 1.0.0"
+git push origin master
+```
+
+## 当前收录
+
+| Id | 名称 | 版本 | host | 说明 |
+|---|---|---|---|---|
+| Ext.AiAssistant | AI 助手 | 1.0.0 | platform | 流式对话 + Function Calling,支持电脑控制与平台命令桥 |
+| Ext.Cameras | 相机 | 1.0.0 | platform | 相机设备管理、实时预览、曝光增益调节(内置 UVC/仿真) |
+| Ext.ColorSchemes | 配色 | 1.0.0 | platform | 七套种子色配色主题,外观→配色切换 |
+| Ext.Light | 光源 | 1.0.0 | platform | 多通道亮度控制、全开全关,内置仿真和 KCS 串口驱动 |
+| Ext.FlowWork | 流程画布 | 1.0.0 | platform | 数据库、流程引擎与画布、产品管理、安全域 |
+| Ext.Navigation.Classic | 导航(经典顶部) | 1.0.0 | platform | 经典顶部横条导航壳,支持外观参数即时调整 |
+| Ext.Welcome | 首页 | 1.0.0 | platform | 产品栏/画面区 + 生产统计与运行日志面板 |
+| Plugins.Camera.Hikvision | 海康相机驱动 | 1.0.0 | cameras | 海康机器人(Hikrobot MVS)相机驱动 |
+| Plugins.Camera.Basler | Basler 相机驱动 | 1.0.0 | cameras | Basler Pylon 相机驱动 |
+| Plugins.Camera.Ahua | 华睿相机驱动 | 1.0.0 | cameras | 华睿(iRayple MVSDK)相机驱动 |

+ 0 - 226
_host/build/regen-gallery-index.ps1

@@ -1,226 +0,0 @@
-# Regenerate gallery\index.json from every *.zip under gallery\packages\.
-#
-# Layout convention: gallery\packages\<host>\<id>\<id>-<version>.zip
-# Each zip must contain a manifest.json at its root. The manifest's "host"
-# field decides which host tab the package belongs to (missing => platform,
-# backward compatible with plain extension packages).
-#
-# Rules (do not relax):
-#  - All plugin packages live in this single index.json.
-#  - 0.1.0 skeleton packages are excluded (removed from the product).
-#  - downloadUrl is stored relative (packages\...); GalleryClient rewrites it
-#    against whatever source root is actually configured.
-#
-# Invoked by each project's post-build target: parallel builds launch several
-# copies at once, so the whole regeneration runs under a named mutex.
-$ErrorActionPreference = 'Stop'
-
-function Invoke-WithRetry
-{
-    param([scriptblock]$Action, [int]$Attempts = 5)
-    for ($i = 1; ; $i++)
-    {
-        try { return & $Action }
-        catch
-        {
-            if ($i -ge $Attempts) { throw }
-            Start-Sleep -Milliseconds (250 * $i)
-        }
-    }
-}
-
-function Invoke-Regenerate
-{
-    $root = Split-Path -Parent $PSScriptRoot
-    $galleryDir = Join-Path $root 'gallery'
-    $packagesDir = Join-Path $galleryDir 'packages'
-    $indexPath = Join-Path $galleryDir 'index.json'
-
-    if (-not (Test-Path $packagesDir)) {
-        throw "packages directory not found: $packagesDir"
-    }
-
-    Add-Type -AssemblyName System.IO.Compression.FileSystem
-
-    $sha = [System.Security.Cryptography.SHA256]::Create()
-    $entries = New-Object System.Collections.Generic.List[object]
-    $seen = @{}
-
-    $zipFiles = Invoke-WithRetry { Get-ChildItem -Path $packagesDir -Recurse -Filter '*.zip' | Sort-Object FullName }
-
-    foreach ($zipFile in $zipFiles) {
-        # The producing build may still be flushing the zip; retry the open.
-        $zip = Invoke-WithRetry { [System.IO.Compression.ZipFile]::OpenRead($zipFile.FullName) }
-        try {
-            $manifestEntry = $zip.Entries |
-                Where-Object { $_.FullName -replace '\\', '/' -eq 'manifest.json' } |
-                Select-Object -First 1
-            if (-not $manifestEntry) {
-                Write-Warning "skip (no manifest.json at root): $($zipFile.Name)"
-                continue
-            }
-
-            $reader = New-Object System.IO.StreamReader($manifestEntry.Open(), [System.Text.Encoding]::UTF8)
-            try { $manifest = $reader.ReadToEnd() } finally { $reader.Dispose() }
-
-            $j = $manifest | ConvertFrom-Json
-            if (-not $j.id -or -not $j.version) {
-                Write-Warning "skip (manifest missing id/version): $($zipFile.Name)"
-                continue
-            }
-            if ($j.version -eq '0.1.0') {
-                Write-Host "exclude 0.1.0 package: $($j.id)"
-                continue
-            }
-
-            $dedupKey = "$($j.id)@$($j.version)"
-            if ($seen.ContainsKey($dedupKey)) {
-                Write-Warning "skip duplicate: $dedupKey ($($zipFile.Name))"
-                continue
-            }
-            $seen[$dedupKey] = $true
-
-            # Hash + size come from the packaged zip itself.
-            $fs = Invoke-WithRetry { [System.IO.File]::OpenRead($zipFile.FullName) }
-            try {
-                $hashBytes = $sha.ComputeHash($fs)
-                $size = $fs.Length
-            } finally { $fs.Dispose() }
-            $hashHex = ([System.BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()
-
-            $relativeUrl = $zipFile.FullName.Substring($galleryDir.Length + 1) -replace '\\', '/'
-
-            $entry = [ordered]@{
-                id          = [string]$j.id
-                name        = [string]$j.name
-                version     = [string]$j.version
-                publisher   = if ($j.publisher) { [string]$j.publisher } else { $null }
-                description = if ($j.description) { [string]$j.description } else { $null }
-                downloadUrl = $relativeUrl
-                sha256      = $hashHex
-                size        = $size
-            }
-            if ($j.host) { $entry['host'] = [string]$j.host }
-            if ($j.category) { $entry['category'] = [string]$j.category }
-            if ($j.engine) { $entry['engine'] = [string]$j.engine }
-            if ($j.kind) { $entry['kind'] = [string]$j.kind }
-            # 构建形态(platform 包区分 Debug/Release 用;缺省不带该字段 → 消费方按 Release 处理)
-            if ($j.flavor) { $entry['flavor'] = [string]$j.flavor }
-            if ($j.guid) { $entry['guid'] = [string]$j.guid }
-            # 本版特性:来自包内 manifest.json 的 notes 字段(扩展管理页"本版特性"区展示)
-            if ($j.notes) { $entry['notes'] = [string]$j.notes }
-            $tfLine = ($manifest -split "`r?`n") | Where-Object { $_ -match 'targetFramework' } | Select-Object -First 1
-            if ($tfLine) { $entry['targetFramework'] = ($tfLine -replace '.*:\s*"([^"]+)".*', '$1') }
-            if ($j.references) { $entry['references'] = @($j.references) }
-            if ($j.dependencies -and @($j.dependencies).Count -gt 0) {
-                $entry['dependencies'] = @($j.dependencies)
-            }
-            # 运行环境(包内 manifest.json 的 targetFramework:在线仓库尚未下载包时的唯一来源);
-            # 取值直接按 JSON 文本提取,避免依赖 PS 对象成员访问(在本机环境下试过会静默取到空值)
-            # 宿主要求:{ id, minVersion } → "TeamPLT>=2.1.0"
-            if ($j.requiresHost) {
-                $rhText = if ($j.requiresHost.minVersion) { "$($j.requiresHost.id)>=$($j.requiresHost.minVersion)" } else { [string]$j.requiresHost.id }
-                $entry['requiresHost'] = $rhText
-            }
-            if ($j.arch -and @($j.arch).Count -gt 0) { $entry['arch'] = @($j.arch) }
-            if ($j.nativeDependencies -and @($j.nativeDependencies).Count -gt 0) {
-                $entry['nativeDependencies'] = @($j.nativeDependencies)
-            }
-
-            # versions[]:index.json 契约里"多版本"就装在这个数组里(SDK 的 GalleryPackage.AllVersions 按它读取),
-            # 平台扩展管理页的"版本"下拉也只认这一个来源 —— 缺了它,界面上永远只显示顶层 version 那一版,
-            # 旧版本即使 zip 就在仓库里也选不到、装不了、回滚不了。
-            $entry['versions'] = @([ordered]@{
-                version     = [string]$j.version
-                downloadUrl = $relativeUrl
-                sha256      = $hashHex
-                size        = $size
-                releasedAt  = (Get-Date).ToString('yyyy-MM-dd')
-                status      = 'stable'
-                notes       = if ($j.notes) { [string]$j.notes } else { $null }
-            })
-
-            $entries.Add([PSCustomObject]$entry)
-            Write-Host ("indexed {0} {1} [{2}]" -f $j.id, $j.version, $(if ($j.host) { $j.host } else { 'platform' }))
-        } finally {
-            $zip.Dispose()
-        }
-    }
-    $sha.Dispose()
-
-    # 合并同 id 的多条为一个条目:packages[] 里每个 id 只应出现一次,多版本装进各自的 versions[]。
-    # 不做这步的话,同 id 的每个版本各占一条,平台按 id 取一条展示 —— 表现为"版本下拉只剩最新一版"。
-    function Get-VersionKey([string]$v) {
-        $parsed = $null
-        if ([version]::TryParse($v, [ref]$parsed)) { return $parsed }
-        return [version]'0.0'
-    }
-
-    $byId = [ordered]@{}
-    foreach ($e in $entries) {
-        $key = [string]$e.id
-        if ($byId.Contains($key)) {
-            $cur = $byId[$key]
-            $mergedVersions = @($cur.versions) + @($e.versions)
-
-            # 顶层字段必须对齐到"更高的版本",而不是沿用先遇到的那条。
-            # dependencies / references / kind / description 等各版本可能不同 ——
-            # 例如 1.0.0 依赖后来废弃的契约包,1.0.1 改依赖新契约包。
-            # 若顶层留在旧版(文件序通常旧版在前),平台按 id 读到的就是旧依赖,
-            # 表现为"明明装了新版,却提示旧依赖未安装"。故版本更高时整体替换,再挂回累计版本列表。
-            if ((Get-VersionKey $e.version) -gt (Get-VersionKey $cur.version)) {
-                $cur = $e.PSObject.Copy()
-            }
-            $cur.versions = $mergedVersions
-            $byId[$key] = $cur
-        } else {
-            $byId[$key] = $e
-        }
-    }
-
-    $merged = New-Object System.Collections.Generic.List[object]
-    foreach ($key in $byId.Keys) {
-        $p = $byId[$key]
-        $versions = @($p.versions | Sort-Object -Property @{ Expression = {
-            $v = $null
-            if ([version]::TryParse([string]$_.version, [ref]$v)) { $v } else { [version]'0.0' }
-        } } -Descending)
-        if ($versions.Count -eq 0) { $merged.Add($p); continue }
-        # 顶层字段对齐到最高版本,保证只读顶层字段的旧消费方也拿到最新版。
-        $top = $versions[0]
-        $p.versions    = $versions
-        $p.version     = [string]$top.version
-        $p.downloadUrl = $top.downloadUrl
-        $p.sha256      = $top.sha256
-        $p.size        = $top.size
-        if (-not $p.notes -and $top.notes) { $p.notes = $top.notes }
-        $merged.Add($p)
-    }
-
-    # Stable order: host (platform first), then id.
-    $sorted = @($merged | Sort-Object @{ Expression = { if ($_.host) { $_.host } else { 'platform' } } }, @{ Expression = { $_.id } })
-
-    $index = [ordered]@{
-        schemaVersion = 1
-        generatedAt   = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
-        packages      = $sorted
-    }
-
-    $json = $index | ConvertTo-Json -Depth 10
-    Invoke-WithRetry { [System.IO.File]::WriteAllText($indexPath, $json, (New-Object System.Text.UTF8Encoding($false))) }
-    Write-Host "REGEN DONE: $($sorted.Count) package(s) -> $indexPath"
-}
-
-# Serialize parallel post-build invocations within this session.
-$mutex = New-Object System.Threading.Mutex($false, 'Local\TeamAAS-regen-gallery-index')
-$hasHandle = $false
-try {
-    if (-not $mutex.WaitOne([TimeSpan]::FromMinutes(5))) {
-        throw 'timed out waiting for gallery index lock'
-    }
-    $hasHandle = $true
-    Invoke-Regenerate
-} finally {
-    if ($hasHandle) { $mutex.ReleaseMutex() }
-    $mutex.Dispose()
-}

+ 64 - 1
index.json

@@ -1,6 +1,6 @@
 {
     "schemaVersion":  1,
-    "generatedAt":  "2026-10-02T12:05:23Z",
+    "generatedAt":  "2026-10-02T15:21:44Z",
     "packages":  [
                      {
                          "id":  "Plugins.Camera",
@@ -652,6 +652,15 @@
                                               "status":  "stable",
                                               "notes":  "• TCP 客户端/服务器、UDP、串口透传\n• Modbus TCP 设备管理、地址读写与状态监视\n• 内置仿真 PLC,其他品牌经 IPlcDriverRegistry 插件接入"
                                           },
+                                          {
+                                              "version":  "1.0.1",
+                                              "downloadUrl":  "packages/platform/Ext.Communication/Ext.Communication-1.0.1.zip",
+                                              "sha256":  "76e8e122d8b0c0555df4f995b33cff853e1ab889aeefa203aebb01ea0c93cc49",
+                                              "size":  2233758,
+                                              "releasedAt":  "2026-10-02",
+                                              "status":  "stable",
+                                              "notes":  "• TCP 客户端/服务器、UDP、串口透传\n• Modbus TCP 设备管理、地址读写与状态监视\n• 内置仿真 PLC,其他品牌经 IPlcDriverRegistry 插件接入"
+                                          },
                                           {
                                               "version":  "1.0.0",
                                               "downloadUrl":  "packages/platform/Ext.Communication/Ext.Communication-1.0.0.zip",
@@ -985,6 +994,15 @@
                                               "releasedAt":  "2026-10-02",
                                               "status":  "stable",
                                               "notes":  "• 与平台 2.1.8 配套分发\n• 含 TeamPLT.Contracts.dll(netstandard2.0)"
+                                          },
+                                          {
+                                              "version":  "1.0.0",
+                                              "downloadUrl":  "packages/contracts/TeamPLT.Contracts/TeamPLT.Contracts-1.0.0.zip",
+                                              "sha256":  "705e066cb0d4fbed911633a246de6f4a8ab680679c6f89c35bb735f497e67c4a",
+                                              "size":  16139,
+                                              "releasedAt":  "2026-10-02",
+                                              "status":  "stable",
+                                              "notes":  "契约层独立发布:更新它不必重编平台 exe。运行期 dll 被 CLR 锁定时会暂存 .pending,重启后生效。"
                                           }
                                       ]
                      },
@@ -1016,6 +1034,24 @@
                                               "status":  "stable",
                                               "notes":  "• 机器码改为只认主板固件身份(主板序列号/BIOS/整机 UUID/CPU):换硬盘、重装系统、换网卡不再需要重新激活(历史密钥需按新机器码重签)\n• 授权激活与管理员登录窗四语言文案补全\n• 日志双语化(本地化日志副本)\n• Gallery 源与扩展安装/更新流程优化"
                                           },
+                                          {
+                                              "version":  "2.1.4",
+                                              "downloadUrl":  "packages/platform/TeamPLT.Platform/TeamPLT.Platform-2.1.4.zip",
+                                              "sha256":  "749c4246b970b7ecaa1dd97890e6fc1e5d889667cb9c9416bbfbd64f6402c271",
+                                              "size":  5336110,
+                                              "releasedAt":  "2026-10-02",
+                                              "status":  "stable",
+                                              "notes":  "• 版本 2.1.4:通讯/扩展加载修复,Contracts 批量读写接口 IPlcBatchDevice 生效\n• 与内部构建 Debug 同源同版本,区别只在构建形态"
+                                          },
+                                          {
+                                              "version":  "2.1.3",
+                                              "downloadUrl":  "packages/platform/TeamPLT.Platform/TeamPLT.Platform-2.1.3.zip",
+                                              "sha256":  "ca60c2509e6da1c252c3974da0785217b8cb1ed3d2086c5ebd206f5741af252f",
+                                              "size":  5166646,
+                                              "releasedAt":  "2026-10-02",
+                                              "status":  "stable",
+                                              "notes":  "• 新增:平台帮助页左侧「语言」切换区,按环境里实际存在的语言资源动态列出,切换即时生效并记忆选择\n• 版本号 2.1.3(与内部构建 Debug 同源同版本,区别只在构建形态)"
+                                          },
                                           {
                                               "version":  "2.1.2",
                                               "downloadUrl":  "packages/platform/TeamPLT.Platform/TeamPLT.Platform-2.1.2.zip",
@@ -1024,6 +1060,15 @@
                                               "releasedAt":  "2026-10-02",
                                               "status":  "stable",
                                               "notes":  "• 软件授权(仅正式版生效):机器码 + 密钥离线激活,可带到期时间与功能子项,授权写系统盘与安装目录无关;未激活单次运行 2 小时\n• 窗体缩放:各窗体按自己的设计尺寸缩放并跟随自身大小实时变化;新增「逐窗体缩放」设置与整体缩放总开关\n• 修复:拖拽预览不跟随鼠标(统一屏幕坐标换算,并不再缩放第三方临时预览窗)\n• 修复:弹窗不再被拉回主窗口中心(沿用系统算好的居中)\n• 优化:小窗体(如关于/登录)不再被放大到占满屏幕"
+                                          },
+                                          {
+                                              "version":  "2.1.1",
+                                              "downloadUrl":  "packages/platform/TeamPLT.Platform/TeamPLT.Platform-2.1.1.zip",
+                                              "sha256":  "45efbaa5c49be11f8516ca4278b3f7f046b53df405913b4b0be56e9dfa93f6e2",
+                                              "size":  5179095,
+                                              "releasedAt":  "2026-10-02",
+                                              "status":  "stable",
+                                              "notes":  "扩展管理页新增平台本体手动更新(检查 → 下载 → 退出替换 → 自动重启);修正在运行的扩展更新时报错「访问被拒绝」;契约出厂自带自愈"
                                           }
                                       ]
                      },
@@ -1055,6 +1100,24 @@
                                               "status":  "stable",
                                               "notes":  "• 与正式版同源同版本(2.1.8),区别只在构建形态(flavor=Debug):不校验授权、保留调试设施\n• 更新按形态匹配:Debug 只认领本包,不会被正式版覆盖(反之亦然)\n• 机器码算法与正式版一致(主板/BIOS/整机 UUID/CPU)"
                                           },
+                                          {
+                                              "version":  "2.1.4",
+                                              "downloadUrl":  "packages/platform/TeamPLT.Platform.Debug/TeamPLT.Platform.Debug-2.1.4.zip",
+                                              "sha256":  "abe0f9d23b1b3c730e007c8f04b3d7a31e57281acc4a9bb6013c5a7e0898a49b",
+                                              "size":  5379106,
+                                              "releasedAt":  "2026-10-02",
+                                              "status":  "stable",
+                                              "notes":  "• 版本 2.1.4:通讯/扩展加载修复,Contracts 批量读写接口 IPlcBatchDevice 生效\n• 与正式版 Release 同源同版本,区别只在构建形态"
+                                          },
+                                          {
+                                              "version":  "2.1.3",
+                                              "downloadUrl":  "packages/platform/TeamPLT.Platform.Debug/TeamPLT.Platform.Debug-2.1.3.zip",
+                                              "sha256":  "205acb71d3b1e366124ce38da215961b64ab49474f48d4c9efe48a8f1ac0a2e3",
+                                              "size":  5210025,
+                                              "releasedAt":  "2026-10-02",
+                                              "status":  "stable",
+                                              "notes":  "• 新增:平台帮助页左侧「语言」切换区,按环境里实际存在的语言资源动态列出,切换即时生效并记忆选择\n• 版本号 2.1.3;与正式版同源同版本,区别只在构建形态(flavor=Debug):不校验授权、保留调试设施\n• 更新按形态匹配:Debug 只认领本包,不会被正式版覆盖(反之亦然)"
+                                          },
                                           {
                                               "version":  "2.1.2",
                                               "downloadUrl":  "packages/platform/TeamPLT.Platform.Debug/TeamPLT.Platform.Debug-2.1.2.zip",

BIN
packages/contracts/TeamPLT.Contracts/TeamPLT.Contracts-1.0.0.zip


+ 27 - 0
packages/platform/Ext.ColorSchemes/README.md

@@ -0,0 +1,27 @@
+# 配色(Ext.ColorSchemes)
+
+TeamPLT 平台的配色主题扩展,内置七套种子色配色方案。
+
+## 配色列表
+
+| Id | 名称 | 特点 |
+|---|---|---|
+| `slate` | 青灰 | 冷灰中性 + 石板蓝主色,常规调试/标定界面 |
+| `industrial` | 工业橙 | 暖橙主色 + 微暖中性,现场强光环境醒目 |
+| `ocean` | 深海蓝 | 深蓝主色 + 青绿强调,监控工位长时间盯屏 |
+| `forest` | 松林绿 | 松绿主色 + 草绿强调,低刺激,视觉检测工位 |
+| `amethyst` | 紫晶 | 紫主色 + 品红强调,暗房/多屏拼接 |
+| `sand` | 暖砂 | 驼棕主色 + 湖蓝强调,暖光车间 |
+| `contrast` | 高对比 | 纯白内容面 + 深灰侧栏,强光/远距离查看 |
+
+## 用法
+
+安装后在平台菜单 **外观 → 配色** 里切换。选择自动记忆,重启后恢复。
+
+## 开发说明
+
+每套配色只需提供 7 个种子色(Primary/Accent/Success/Warning/Danger/Info/Neutral),平台自动派生出全部语义令牌:功能色明暗变体、文本与背景层级、业务状态色、设备状态灯、图表色板。
+
+浅色和深色各提供层级覆盖(Background/Region/SecondaryRegion/ThirdlyRegion/Border),拉开窗口底→侧栏→内容面→分组块的背景层次,避免一片白或一片黑。
+
+深色种子(DarkSeeds)只需指定主色和强调色,其余字段留空自动沿用浅色值。

BIN
packages/platform/Ext.Communication/Ext.Communication-1.0.1.zip


+ 35 - 0
packages/platform/Ext.Light/README.md

@@ -0,0 +1,35 @@
+# 光源(Ext.Light)
+
+TeamPLT 平台的光源控制器扩展,管理多通道光源的亮度控制与全开全关。
+
+## 功能
+
+- **多通道亮度控制**:按通道设置亮度(0~255),支持读回当前值
+- **全开/全关**:一键点亮或熄灭所有通道
+- **仿真光源**:内置 4 通道和 8 通道仿真设备,无硬件即可跑通全链路
+- **KCS 串口驱动**:支持 KDC-12V60W-4T(4 通道)和 KDC3-24V300W-8T(8 通道)控制器
+- **流程节点**:提供"光源控制"流程节点,可在流程图中按名称引用设备
+
+## 支持的设备
+
+| kind | 说明 |
+|---|---|
+| `simulated` | 仿真光源(4 通道) |
+| `simulated8` | 仿真光源(8 通道) |
+| `kcs4` | KCS 串口控制器(4 通道) |
+| `kcs8` | KCS 串口控制器(8 通道) |
+
+## 调试
+
+活动栏点击"光源"进入调试页,可添加设备、连接/断开、拖动滑杆设置通道亮度、读回当前值。状态栏显示光源连接状态(已连接/总数)。
+
+## 流程节点
+
+流程图中拖入"光源控制"节点,选择设备和模式:
+- **设置单通道亮度**:指定通道号和亮度值
+- **全开所有通道**:未点亮的通道设为默认亮度 120
+- **全关所有通道**:所有通道亮度归零
+
+## 配置
+
+设备配置保存在 `Config\light.json`,重启后自动恢复。KCS 设备需指定串口号和波特率(默认 COM3 / 9600)。

BIN
packages/platform/TeamPLT.Platform.Debug/TeamPLT.Platform.Debug-2.1.3.zip


BIN
packages/platform/TeamPLT.Platform.Debug/TeamPLT.Platform.Debug-2.1.4.zip


BIN
packages/platform/TeamPLT.Platform/TeamPLT.Platform-2.1.1.zip


BIN
packages/platform/TeamPLT.Platform/TeamPLT.Platform-2.1.3.zip


BIN
packages/platform/TeamPLT.Platform/TeamPLT.Platform-2.1.4.zip


+ 205 - 0
regen-index.ps1

@@ -0,0 +1,205 @@
+# Regenerate index.json for THIS gallery repository (PLT\Gallery).
+#
+# 为什么自带一份:
+#   主仓库 build\regen-gallery-index.ps1 写死了 "<脚本父目录的父目录>\gallery",
+#   只能服务 TeamAAS-2.1\gallery,无法指向这里。
+#
+# 格式:对齐远端 Gallery 仓库的 GalleryPackage schema(含多版本 versions[]),
+#   以保持内容为准:字段全部来自包内 manifest.json,不再手写,避免 index 与包不一致。
+#   版本历史会继承旧 index 中同 id 的其它版本条目(releasedAt / status / notes 不丢)。
+#
+# 用法(改了 packages\ 下任何 zip 之后执行):
+#   powershell -NoProfile -ExecutionPolicy Bypass -File .\regen-index.ps1
+#
+# 布局约定:packages\<host>\<id>\<id>-<version>.zip(zip 根目录必须含 manifest.json)
+$ErrorActionPreference = 'Stop'
+
+$galleryDir  = $PSScriptRoot
+$packagesDir = Join-Path $galleryDir 'packages'
+$indexPath   = Join-Path $galleryDir 'index.json'
+
+if (-not (Test-Path $packagesDir)) {
+    throw "packages directory not found: $packagesDir"
+}
+
+Add-Type -AssemblyName System.IO.Compression.FileSystem
+
+# 旧 index:仅用于继承版本历史
+$history = @{}
+if (Test-Path $indexPath) {
+    try {
+        $old = (Get-Content $indexPath -Raw -Encoding UTF8) | ConvertFrom-Json
+        foreach ($p in $old.packages) {
+            foreach ($v in $p.versions) {
+                $history["$($p.id)@$($v.version)"] = @{
+                    releasedAt = $v.releasedAt
+                    status     = if ($v.status) { $v.status } else { 'stable' }
+                    notes      = $v.notes
+                }
+            }
+        }
+    } catch {
+        Write-Warning "旧 index.json 解析失败,版本历史不继承:$($_.Exception.Message)"
+    }
+}
+
+$today   = (Get-Date).ToString('yyyy-MM-dd')
+$sha     = [System.Security.Cryptography.SHA256]::Create()
+$entries = New-Object System.Collections.Generic.List[object]
+$seen    = @{}
+
+foreach ($zipFile in (Get-ChildItem -Path $packagesDir -Recurse -Filter '*.zip' | Sort-Object FullName)) {
+    $zip = [System.IO.Compression.ZipFile]::OpenRead($zipFile.FullName)
+    try {
+        $manifestEntry = $zip.Entries |
+            Where-Object { $_.FullName -replace '\\', '/' -eq 'manifest.json' } |
+            Select-Object -First 1
+        if (-not $manifestEntry) {
+            Write-Warning "skip (no manifest.json at root): $($zipFile.Name)"
+            continue
+        }
+
+        $reader = New-Object System.IO.StreamReader($manifestEntry.Open(), [System.Text.Encoding]::UTF8)
+        try { $manifest = $reader.ReadToEnd() } finally { $reader.Dispose() }
+
+        $j = $manifest | ConvertFrom-Json
+        if (-not $j.id -or -not $j.version) {
+            Write-Warning "skip (manifest missing id/version): $($zipFile.Name)"
+            continue
+        }
+
+        $key = "$($j.id)@$($j.version)"
+        if ($seen.ContainsKey($key)) {
+            Write-Warning "skip duplicate: $key ($($zipFile.Name))"
+            continue
+        }
+        $seen[$key] = $true
+
+        $fs = [System.IO.File]::OpenRead($zipFile.FullName)
+        try {
+            $hashBytes = $sha.ComputeHash($fs)
+            $size = $fs.Length
+        } finally { $fs.Dispose() }
+        $hashHex = ([System.BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()
+
+        # downloadUrl 存相对路径:GalleryClient 会按实际源根拼接,源换域名也不用改包。
+        $relativeUrl = $zipFile.FullName.Substring($galleryDir.Length + 1) -replace '\\', '/'
+
+        $hostName = if ($j.host) { [string]$j.host } else { 'platform' }
+        $kind     = if ($j.kind) { [string]$j.kind } else { 'extension' }
+        $requires = $null
+        if ($j.requiresHost) {
+            $requires = if ($j.requiresHost.minVersion) {
+                "$($j.requiresHost.id)>=$($j.requiresHost.minVersion)"
+            } else { [string]$j.requiresHost.id }
+        }
+
+        $prev = $history[$key]
+        $versionEntry = [ordered]@{
+            version     = [string]$j.version
+            downloadUrl = $relativeUrl
+            sha256      = $hashHex
+            size        = $size
+            releasedAt  = if ($prev) { $prev.releasedAt } else { $today }
+            status      = if ($prev) { $prev.status } else { 'stable' }
+            # 本版特性优先取被发布物料自己的 manifest.notes(每个版本各自的说明),
+            # 其次继承旧 index 里的同版本记录,最后才是兜底文案。
+            notes       = if ($j.notes) { [string]$j.notes } elseif ($prev -and $prev.notes) { $prev.notes } else { '首次收录' }
+        }
+
+        $entry = [ordered]@{
+            id              = [string]$j.id
+            name            = [string]$j.name
+            version         = [string]$j.version
+            publisher       = if ($j.publisher) { [string]$j.publisher } else { $null }
+            kind            = $kind
+            host            = $hostName
+            description     = if ($j.description) { [string]$j.description } else { $null }
+            notes           = if ($j.notes) { [string]$j.notes } else { $null }
+            downloadUrl     = $relativeUrl
+            sha256          = $hashHex
+            size            = $size
+            references      = @($j.references)
+            targetFramework = if ($j.targetFramework) { [string]$j.targetFramework } else { $null }
+            requiresHost    = $requires
+            arch            = @($j.arch)
+            dependencies    = @($j.dependencies)
+            versions        = @($versionEntry)
+        }
+
+        $entries.Add([PSCustomObject]$entry)
+        Write-Host ("indexed {0} {1} [{2}]  {3} bytes  {4}" -f $j.id, $j.version, $hostName, $size, $hashHex)
+    } finally {
+        $zip.Dispose()
+    }
+}
+$sha.Dispose()
+
+# ---------------------------------------------------------------------------
+# 合并同 id 的多条为单一条目:
+#   index.json 的契约是 packages[] 每个 id 只出现一次,多版本装在 versions[] 里
+#   (SDK 侧 GalleryPackage.AllVersions 就按这个读)。
+#   若同 id 各版本各占一条,平台按 id 取一条展示 —— 表现为"版本下拉只有最新一版",
+#   旧版本永远选不到。这里按 id 归并,并把顶层字段对齐到最高版本。
+# ---------------------------------------------------------------------------
+function Get-VersionKey([string]$v) {
+    $parsed = $null
+    if ([version]::TryParse($v, [ref]$parsed)) { return $parsed }
+    return [version]'0.0'
+}
+
+$byId = [ordered]@{}
+foreach ($e in $entries) {
+    $id = [string]$e.id
+    if ($byId.Contains($id)) {
+        $cur = $byId[$id]
+        $mergedVersions = @($cur.versions) + @($e.versions)
+
+        # 顶层字段必须对齐到"更高的版本",而不是沿用先遇到的那条。
+        # dependencies / references / kind / description 等各版本可能不同 ——
+        # 例如 1.0.0 依赖后来废弃的契约包,1.0.1 改依赖新契约包。
+        # 若顶层留在旧版(文件序通常旧版在前),平台按 id 读到的就是旧依赖,
+        # 表现为"明明装了新版,却提示旧依赖未安装"。故版本更高时整体替换,再挂回累计版本列表。
+        if ((Get-VersionKey $e.version) -gt (Get-VersionKey $cur.version)) {
+            $cur = $e.PSObject.Copy()
+        }
+        $cur.versions = $mergedVersions
+        if (-not $cur.notes -and $e.notes) { $cur.notes = $e.notes }
+        $byId[$id] = $cur
+    } else {
+        $byId[$id] = $e
+    }
+}
+
+$merged = New-Object System.Collections.Generic.List[object]
+foreach ($id in $byId.Keys) {
+    $p = $byId[$id]
+    $versions = @($p.versions | Sort-Object -Property @{ Expression = {
+        $v = $null
+        if ([version]::TryParse([string]$_.version, [ref]$v)) { $v } else { [version]'0.0' }
+    } } -Descending)
+    if ($versions.Count -eq 0) { continue }
+    $top = $versions[0]
+    $p.versions    = $versions
+    $p.version     = [string]$top.version
+    $p.downloadUrl = $top.downloadUrl
+    $p.sha256      = $top.sha256
+    $p.size        = $top.size
+    $p.notes       = if ($p.notes) { $p.notes } else { $top.notes }
+    $merged.Add([PSCustomObject]$p)
+}
+
+# Stable order: host (platform first), then id.
+$sorted = @($merged | Sort-Object @{ Expression = { $_.host } }, @{ Expression = { $_.id } })
+
+$index = [ordered]@{
+    schemaVersion = 1
+    generatedAt   = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
+    packages      = $sorted
+}
+
+$json = $index | ConvertTo-Json -Depth 12
+# ConvertTo-Json 会把 ">" 转义成 \u003e(合法 JSON 但可读性差,且与线上文件不一致),还原。
+$json = $json -replace '\\u003e', '>' -replace '\\u003c', '<' -replace '\\u0026', '&'
+[System.IO.File]::WriteAllText($indexPath, $json, (New-Object System.Text.UTF8Encoding($false)))
+Write-Host "REGEN DONE: $($sorted.Count) package(s) -> $indexPath"

+ 82 - 0
sync-index.ps1

@@ -0,0 +1,82 @@
+<#
+.SYNOPSIS
+    按【发布仓里真实存在的包】重算 index.json 的 size / sha256。
+
+.DESCRIPTION
+    为什么必须有这一步:
+      构建机上的 gallery 源(TeamAAS-2.1\gallery)与发布仓(本目录)是两个独立的目录,
+      长期各自打包、各自推送 —— 两边同名包常常不是同一份文件。
+      而打包时会按【本地源】全量 REGEN 索引,若把这份索引直接推到发布仓,
+      客户端就会拿本地源记录的 size/sha256 去校验发布仓里的另一个文件,
+      安装时报 "大小不匹配:期望 X 字节,实际 Y 字节"。
+
+      因此:每次往发布仓放包之后、提交索引之前,必须跑一次本脚本,
+      让索引里的 size/sha256 与发布仓里实际的文件严格一致。
+
+    用法:
+      .\sync-index.ps1            # 校正并打印改动数量
+      .\sync-index.ps1 -Check     # 只检查,不写文件(适合在提交前做校验/输出不符条目数)
+
+.NOTES
+    只改 size / sha256 两个字段,其余元数据(名称/描述/依赖/版本历史)保持原样。
+    编码按原文件的 BOM 状态原样写回,避免把 UTF-8 内容写坏。
+#>
+param(
+    [switch]$Check
+)
+
+$ErrorActionPreference = 'Stop'
+
+$root = $PSScriptRoot
+$idxPath = Join-Path $root 'index.json'
+
+if (-not (Test-Path $idxPath)) { throw "未找到索引文件:$idxPath" }
+
+# 原样保持 BOM 状态
+$bytes = [IO.File]::ReadAllBytes($idxPath)
+$hasBom = ($bytes.Length -ge 3 -and $bytes[0] -eq 239 -and $bytes[1] -eq 187 -and $bytes[2] -eq 191)
+$text = [IO.File]::ReadAllText($idxPath, [Text.Encoding]::UTF8)
+
+# 发布仓里真实存在的包
+$files = @{}
+foreach ($p in (Get-ChildItem (Join-Path $root 'packages') -Recurse -Filter '*.zip' -ErrorAction SilentlyContinue)) {
+    $rel = $p.FullName.Substring($root.Length).TrimStart('\', '/') -replace '\\', '/'
+    $files[$rel] = $p
+}
+
+# 索引里引用到的包路径
+$refs = [regex]::Matches($text, '"downloadUrl":\s*"([^"]+)"') |
+        ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique
+
+$fixed = 0
+$missing = @()
+
+foreach ($rel in $refs) {
+    if (-not $files.ContainsKey($rel)) { $missing += $rel; continue }
+
+    $f = $files[$rel]
+    $sha = (Get-FileHash $f.FullName -Algorithm SHA256).Hash.ToLower()
+    $size = $f.Length
+    $esc = [regex]::Escape($rel)
+
+    # 结构固定为:downloadUrl → sha256 → size
+    $pattern = '("downloadUrl":\s*"' + $esc + '",\s*"sha256":\s*")[0-9a-fA-F]*(",\s*"size":\s*)\d+'
+    $new = [regex]::Replace($text, $pattern, ('${1}' + $sha + '${2}' + $size))
+    if ($new -ne $text) { $fixed++ }
+    $text = $new
+}
+
+if ($Check) {
+    Write-Host ("不符条目: " + $fixed + " 处")
+} else {
+    [IO.File]::WriteAllText($idxPath, $text, (New-Object Text.UTF8Encoding($hasBom)))
+    Write-Host ("索引已按发布仓实际包校正: " + $fixed + " 处(BOM=" + $hasBom + ")")
+}
+
+if ($missing.Count -gt 0) {
+    Write-Host "警告:索引中有 $($missing.Count) 个条目指向发布仓不存在的包(客户端安装会 404):" -ForegroundColor Yellow
+    $missing | Sort-Object -Unique | ForEach-Object { Write-Host ("   " + $_) -ForegroundColor Yellow }
+}
+
+if ($Check -and $fixed -gt 0) { exit 1 }
+exit 0