Fix inspector undo, time changes and legacy disabled-state migration; verify secure template and 1039 regressions
这个提交包含在:
1 父节点
f4c0d608a4
当前提交
f79d24eb01
19 个文件已更改
+298
-15
没有与搜索条件相匹配的文件
@@ -41,4 +41,4 @@ xcodebuild -project beatygril/beatygril.xcodeproj -scheme 'Wrapper Application'
|
||||
|
||||
使用AppKit自绘卡片、滑块和开关,原生数值输入、肤色圆点、单项/分组启停及重置。FCP仅发布自定义面板210和两项版权信息,撤掉平铺参数与手动分析板块。新面板使用独立NSSecureCoding状态对象完整保存设置,支持复制、撤销数据和数值插值;未编辑时保持旧文档参数值。旧分析数据不参与实时渲染。
|
||||
|
||||
`./scripts/test-inspector.sh`验证参数路由、保存恢复、事务、绕过/重置和离屏布局;`python3 scripts/inspector-template.py audit Templates/大鱼人像美化/大鱼人像美化.moef`验证模板自定义数据和发布入口。离屏截图和模拟宿主检查不能替代Motion/FCP的实际嵌入、键盘/VoiceOver和工程重开验收,仍由用户自测。当前阶段尚未替换已安装1.0.6。
|
||||
`./scripts/test-inspector.sh`验证参数路由、保存恢复、事务、绕过/重置和离屏布局;`python3 scripts/inspector-template.py audit Templates/大鱼人像美化/大鱼人像美化.moef`验证模板自定义数据和发布入口。离屏截图和模拟宿主检查不能替代Motion/FCP的实际嵌入、键盘/VoiceOver和工程重开验收,仍由用户自测。完整1039项回归通过,Inspector/宿主适配/OSC启用ASan。离屏演示图见[面板预览](docs/verification/v1.1.0/inspector-preview.png)。正式签名公证安装正在准备。
|
||||
+10
@@ -58,3 +58,13 @@ FCP模板从 `Templates/大鱼人像美化/` 恢复,将 `.moef` 和 `Drop Zone
|
||||
`ad54726b17fabd11f46bc6121e664eac38960a23`已独立从上述远程克隆,87文件的byte/blob/mode与14项资源一致,strict/full fsck和559项核心测试通过,无LFS、子模块或APP产物。临时恢复目录已清理,完整报告见`docs/verification/v1.0.6/recovery-ad54726.json`。
|
||||
|
||||
1.0.6已完成正式签名、公证(Accepted ab0271c8-0c4b-4b10-9474-cebe9b55385b)、票据粘附、唯一正式目录安装和配套模板同步;临时APP/旧备份APP/公证ZIP已清理。完整回执及有界副本扫描见`docs/verification/v1.0.6/installed-verification.json`。未操作Motion/FCP界面,最终视觉验收保留为用户自测。
|
||||
|
||||
## 1.1.0 面板与自动处理
|
||||
|
||||
1.1.0(9)使用自有原生卡片面板和PGInspectorState安全归档。源代码、面板资源(全部程序绘制)、Motion模板与自有自定义参数数据均在仓库;新增`./scripts/test-inspector.sh`及`scripts/inspector-template.py audit`验证入口。恢复时需完整保留`.moef`中的自定义参数210、实际dataValue/defaultVal和两项版权发布;不要删掉仍用于旧工程迁移的隐藏参数定义和组路径。不得拿其他插件的自定义归档替换本产品自己的归档。
|
||||
|
||||
新渲染统一实时自动人脸跟随,旧手动分析数据仍保存在旧文档中但不参与本版渲染;主界面不再显示前后分析、目标列表、蒙版调试与平铺控件。三张卡片之外的细调收在更多设置。自定义对象保存完整界面状态,首次编辑前兼容旧参数;数值、开关、启停mask按已测试的复制/插值规则恢复。真实宿主UI及工程重开仍需用户自测。
|
||||
|
||||
正式构建、公证、安装仍使用同一`python3 scripts/deliver.py --wait-for-hosts --user-tests`,保持固定Bundle ID与APP路径,仅在用户自行退出Motion/FCP后替换,最终版本节点与回执随交付记录补齐。
|
||||
|
||||
兼容边界:未编辑面板时读取旧文档原参数;首次编辑会把当前数值采集为完整面板状态。旧版各项独立关键帧曲线尚未自动迁移为新面板关键帧,不宣称编辑前后任意旧动画逐帧等价。面板本身的数值插值与开关保持已做离屏验证,实际宿主关键帧交互仍待用户测试。
|
||||
@@ -153,5 +153,38 @@
|
||||
"malformedOrVendorArchiveRejectionCases": 5,
|
||||
"foundationOuterSecureDecode": "pass",
|
||||
"innerClassDecode": "model generates own archive; structure decoded and exact generated bytes preserved; native host instantiation pending user"
|
||||
},
|
||||
"automatedInspectorValidation": {
|
||||
"scope": "offscreen AppKit inspector, real production controller and mocked host callbacks; no Motion/FCP UI",
|
||||
"fullChecks": 1039,
|
||||
"counts": {
|
||||
"beauty": 49,
|
||||
"face": 116,
|
||||
"thinChin": 394,
|
||||
"hostAdapterASan": 248,
|
||||
"oscASan": 98,
|
||||
"inspectorASan": 134
|
||||
},
|
||||
"nativeVisualQA": "pending user",
|
||||
"manualAnalysisUI": "removed; legacy IDs hidden and buttons disabled",
|
||||
"renderMode": "realtime automatic follow; stored manual geometry never used",
|
||||
"templatePublishedEntries": 3,
|
||||
"offscreenWidths": [
|
||||
280,
|
||||
320,
|
||||
380,
|
||||
520
|
||||
],
|
||||
"independentViewChecks": 27,
|
||||
"fixedRegressions": [
|
||||
"shape group bypass clears face-box overlay",
|
||||
"disabled legacy detail effects stay disabled on first edit",
|
||||
"legacy global smoothing remains enabled when its old local group was off",
|
||||
"new gesture reads current host after immediate undo",
|
||||
"pending synchronization is bound to CMTime",
|
||||
"same-time host change refreshes without waiting",
|
||||
"explicit numeric input is never skipped due to stale UI cache"
|
||||
],
|
||||
"archives": "Own PGInspectorState secure payload; Foundation outer and inner decode and Motion alphabet roundtrip tested"
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ FOUNDATION_EXPORT PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings
|
||||
@property (nonatomic, copy, readonly) NSString *statusMessage;
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager;
|
||||
- (void)refresh;
|
||||
- (void)documentDidChange;
|
||||
- (void)beginInteraction;
|
||||
- (void)endInteraction;
|
||||
- (BOOL)setValue:(double)value forParameter:(UInt32)identifier;
|
||||
|
||||
@@ -116,7 +116,7 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat
|
||||
}
|
||||
if(![state valueForParameter:PGSmoothingEnabled])s.amount=s.midtoneDetail=s.highPassDetail=0;
|
||||
if(![state valueForParameter:PGColorEnabled]) {s.skinBrightness=s.skinHue=s.skinSaturation=s.skinEvenTone=s.teethWhitening=s.eyeBrightening=0;s.skinTonePreset=0;}
|
||||
if(![state valueForParameter:PGShapeEnabled])s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;
|
||||
if(![state valueForParameter:PGShapeEnabled]) {s.faceThin=s.faceBigEye=s.faceShort=s.faceNarrow=s.faceChin=s.faceV=s.faceJaw=s.faceCheekbone=s.faceSmallHead=0;s.showTrackBox=0;}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat
|
||||
@property (nonatomic,strong) id<FxCustomParameterActionAPI_v4> action;
|
||||
@property (nonatomic) BOOL interacting;
|
||||
@property (nonatomic) NSTimeInterval pendingUntil;
|
||||
@property (nonatomic) CMTime pendingTime;
|
||||
@end
|
||||
@implementation PGInspectorController
|
||||
- (instancetype)initWithAPIManager:(id<PROAPIAccessing>)manager {
|
||||
@@ -145,6 +146,7 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat
|
||||
}
|
||||
- (void)finish {if(_interacting){_interacting=NO;[_action endAction:self];_action=nil;}}
|
||||
- (void)readAtTime:(CMTime)time {
|
||||
if(_pendingUntil && CMTimeCompare(time,_pendingTime)!=0)_pendingUntil=0;
|
||||
id<FxParameterRetrievalAPI_v6> api=[_manager apiForProtocol:@protocol(FxParameterRetrievalAPI_v6)];
|
||||
NSObject *object=nil;[api getCustomParameterValue:&object fromParameter:PGInspectorPanel atTime:time];
|
||||
if([object isKindOfClass:PGInspectorState.class] && [(PGInspectorState *)object hasEdits]) {
|
||||
@@ -162,16 +164,31 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat
|
||||
else [api getFloatValue:&value fromParameter:key atTime:time];
|
||||
values[@(key)]=@(value);
|
||||
}
|
||||
_state=[PGInspectorState stateWithValues:values disabledParameters:[NSSet set] hasEdits:NO];
|
||||
BOOL legacyDetail=YES;[api getBoolValue:&legacyDetail fromParameter:PGDetailEnabled atTime:time];
|
||||
NSMutableSet *disabled=legacyDetail?[NSMutableSet set]:[NSMutableSet setWithObjects:@(PGHighPass),@(PGSharpness),nil];
|
||||
// Earlier versions put high-pass smoothing under a different group. Keep
|
||||
// its effective state when importing a document whose local-skin group was
|
||||
// off, rather than turning a still-active global effect off on first edit.
|
||||
if(![values[@(PGSmoothingEnabled)] boolValue] && legacyDetail && [values[@(PGHighPass)] doubleValue]>0) {
|
||||
values[@(PGSmoothingEnabled)]=@YES;
|
||||
[disabled addObject:@(PGMidtone)];[disabled addObject:@(PGAmount)];
|
||||
}
|
||||
_state=[PGInspectorState stateWithValues:values disabledParameters:disabled hasEdits:NO];
|
||||
}
|
||||
- (void)refresh {
|
||||
if(_interacting)return;
|
||||
if(![self start])return;
|
||||
@try {[self readAtTime:[_action currentTime]];_statusMessage=@"";} @finally {[self finish];}
|
||||
}
|
||||
- (void)documentDidChange {if(_interacting)return;_pendingUntil=0;[self refresh];}
|
||||
- (void)beginInteraction {
|
||||
if(_interacting)return;
|
||||
if([self start])[self readAtTime:[_action currentTime]];
|
||||
if([self start]) {
|
||||
// A new gesture starts from the authoritative document, including an
|
||||
// immediate undo. The short pending hold only prevents display flicker;
|
||||
// it must never resurrect an undone value in a subsequent edit.
|
||||
_pendingUntil=0;[self readAtTime:[_action currentTime]];
|
||||
}
|
||||
}
|
||||
- (void)endInteraction {[self finish];}
|
||||
- (BOOL)commit:(PGInspectorState *)next {
|
||||
@@ -190,7 +207,7 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat
|
||||
else if([d[@"kind"] isEqual:@"toggle"] || [d[@"kind"] isEqual:@"master"]) [api setBoolValue:value!=0 toParameter:key atTime:time];
|
||||
else [api setFloatValue:value toParameter:key atTime:time];
|
||||
}
|
||||
_state=next;_pendingUntil=NSDate.timeIntervalSinceReferenceDate+.35;_statusMessage=@"";return YES;
|
||||
_state=next;_pendingUntil=NSDate.timeIntervalSinceReferenceDate+.35;_pendingTime=time;_statusMessage=@"";return YES;
|
||||
}
|
||||
- (BOOL)setValue:(double)value forParameter:(UInt32)identifier {
|
||||
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d || !isfinite(value))return NO;
|
||||
@@ -202,7 +219,11 @@ PGBeautySettings PGSettingsWithInspectorState(PGBeautySettings s,PGInspectorStat
|
||||
}
|
||||
- (BOOL)toggleEnabledForParameter:(UInt32)identifier {
|
||||
NSDictionary *d=PGInspectorDescriptor(identifier);if(!d)return NO;
|
||||
if([d[@"kind"] isEqual:@"master"] || [d[@"kind"] isEqual:@"toggle"])return [self setValue:![self.state valueForParameter:identifier] forParameter:identifier];
|
||||
if([d[@"kind"] isEqual:@"master"] || [d[@"kind"] isEqual:@"toggle"]) {
|
||||
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||
BOOL ok=NO;@try {ok=[self setValue:![_state valueForParameter:identifier] forParameter:identifier];}
|
||||
@finally {if(owned)[self finish];}return ok;
|
||||
}
|
||||
if(![d[@"bypass"] boolValue])return NO;
|
||||
BOOL owned=!_interacting;if(owned)[self beginInteraction];if(!_interacting)return NO;
|
||||
BOOL ok=NO;@try {
|
||||
|
||||
@@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// Also permits a deterministic controller in an offscreen preview or test.
|
||||
- (instancetype)initWithController:(PGInspectorController *)controller;
|
||||
- (void)refreshFromHost;
|
||||
- (void)hostParameterChanged;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -253,7 +253,10 @@ static PGInspectorGlyphButton *PGUIButton(NSString *glyph, NSColor *accent, id t
|
||||
}
|
||||
if(number&&isfinite(number.doubleValue)){
|
||||
double value=number.doubleValue/PGValueScale(_descriptor);value=fmin([_descriptor[@"max"]doubleValue],fmax([_descriptor[@"min"]doubleValue],value));
|
||||
if(fabs(value-[_inspector.controller.state valueForParameter:[_descriptor[@"id"]unsignedIntValue]])>.0000001)[_inspector writeValue:value parameter:[_descriptor[@"id"]unsignedIntValue]];
|
||||
// Commit explicit input even when it equals our last displayed value:
|
||||
// the document may have changed while the field editor held focus.
|
||||
// The controller rereads the host and deduplicates the actual value.
|
||||
[_inspector writeValue:value parameter:[_descriptor[@"id"]unsignedIntValue]];
|
||||
}else NSBeep();
|
||||
_valueField.stringValue=PGFormattedValue(_descriptor,[_inspector.controller.state valueForParameter:[_descriptor[@"id"]unsignedIntValue]]);
|
||||
}
|
||||
@@ -331,6 +334,7 @@ static PGInspectorGlyphButton *PGUIButton(NSString *glyph, NSColor *accent, id t
|
||||
}
|
||||
- (BOOL)hasActiveTextEditor { for(PGInspectorCard *card in _cards)for(PGInspectorRow *row in card.rows)if(row.valueField.currentEditor)return YES;return NO; }
|
||||
- (void)refreshFromHost { if(_interactionDepth || [self hasActiveTextEditor])return;[_controller refresh];[self reloadControls]; }
|
||||
- (void)hostParameterChanged { if(_interactionDepth || [self hasActiveTextEditor])return;[_controller documentDidChange];[self reloadControls]; }
|
||||
- (void)writeValue:(double)value parameter:(UInt32)identifier { [_controller setValue:value forParameter:identifier];[self reloadControls]; }
|
||||
- (void)beginControlInteraction { if(_interactionDepth++==0)[_controller beginInteraction]; }
|
||||
- (void)endControlInteraction { if(!_interactionDepth)return;if(--_interactionDepth==0){[_controller endInteraction];[self refreshFromHost];} }
|
||||
|
||||
@@ -93,6 +93,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
@property BOOL analysisForward;
|
||||
@property (strong) NSArray<NSString *> *loadedTracks;
|
||||
@property BOOL reportedRender;
|
||||
@property (strong) NSHashTable<PGInspectorView *> *inspectorViews;
|
||||
@end
|
||||
|
||||
@implementation beatygrilPlugIn
|
||||
@@ -103,6 +104,7 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
_analysisFaceProcessor=[PGFaceProcessor new];
|
||||
_contexts=[NSMutableDictionary new]; _analysisSettings=PGBeautyDefaults();
|
||||
_analysisTime=kCMTimeZero;
|
||||
_inspectorViews=[NSHashTable weakObjectsHashTable];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -165,7 +167,16 @@ static CIImage *PGImageFromTile(FxImageTile *tile) {
|
||||
return ok ? YES : PGFail(error,@"宿主未能创建完整的人像美化参数。");
|
||||
}
|
||||
- (NSView *)createViewForParameterID:(UInt32)identifier {
|
||||
return identifier==PGInspectorPanel ? [[PGInspectorView alloc] initWithAPIManager:_apiManager] : nil;
|
||||
if(identifier!=PGInspectorPanel)return nil;
|
||||
PGInspectorView *view=[[PGInspectorView alloc] initWithAPIManager:_apiManager];
|
||||
@synchronized(_inspectorViews){[_inspectorViews addObject:view];}return view;
|
||||
}
|
||||
- (BOOL)parameterChanged:(UInt32)identifier atTime:(CMTime)time error:(NSError **)error {
|
||||
// The host callback covers undo and playhead-driven parameter changes. Hop
|
||||
// to AppKit's thread and avoid retaining remote inspector views in render.
|
||||
NSArray *views;@synchronized(_inspectorViews){views=_inspectorViews.allObjects;}
|
||||
for(PGInspectorView *view in views)dispatch_async(dispatch_get_main_queue(), ^{[view hostParameterChanged];});
|
||||
return YES;
|
||||
}
|
||||
- (Class)classForCustomParameterID:(UInt32)identifier {return identifier==PGInspectorPanel?PGInspectorState.class:Nil;}
|
||||
- (NSSet<Class> *)classesForCustomParameterID:(UInt32)identifier {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Beauty renderer: 49 checks, 0 failures (Metal).
|
||||
2026-10-21 22:48:21.085 face-tests[18460:5636636] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
Face processing: 116 checks passed
|
||||
2026-10-21 22:48:24.576 slim-tests[18472:5636747] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
PASS: 394 slim alignment checks; maximum UV oracle error 1.09828022e-05
|
||||
@@ -0,0 +1,21 @@
|
||||
2026-10-21 22:52:28.194 host-tests[18824:5644007] FBBA: creating VNFaceBBoxAligner from VNFaceDetectorRevision2: VNFaceDetectorRevision2
|
||||
Default IOSurface maximum byte difference: 0
|
||||
Alternate destination tag maximum byte difference: 0
|
||||
Anatomy default 0/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy default 0/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy default 1/0 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy default 1/1 max=0 mean=0.0000000 eye=0/0 chin=0 inverted=0
|
||||
Anatomy big-eye 0/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy big-eye 0/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy big-eye 1/0 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy big-eye 1/1 max=2 mean=0.0008917 eye=33510/33880 chin=0 inverted=0
|
||||
Anatomy chin-positive 0/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-positive 0/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-positive 1/0 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-positive 1/1 max=1 mean=0.0001459 eye=0/0 chin=116812 inverted=0
|
||||
Anatomy chin-negative 0/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Anatomy chin-negative 0/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Anatomy chin-negative 1/0 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Anatomy chin-negative 1/1 max=1 mean=0.0000849 eye=0/0 chin=60426 inverted=0
|
||||
Legacy fallback upright comparison: max=2 mean=0.0010357
|
||||
Host adapter: 248 checks, 0 failures
|
||||
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 482 KiB |
@@ -0,0 +1 @@
|
||||
Inspector: 134 checks, 0 failures
|
||||
@@ -0,0 +1,21 @@
|
||||
2026-10-21 22:48:19.739 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
|
||||
2026-10-21 22:48:19.745 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,180) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.198 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.8,0.3) setter=(1152,-432) readback=(0.8,0.3) mode=centered-pixels verified=1
|
||||
2026-10-21 22:48:20.198 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.7,0.4) objectBounds=(-1920,-1080,3840,2160) canvas=(152,78) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.246 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.8,0.3) setter=(0.8,0.3) readback=(0.12345,0.87654) mode=normalized verified=0
|
||||
2026-10-21 22:48:20.247 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.75,0.25) setter=(0.75,0.25) readback=(0.75,0.25) mode=normalized verified=1
|
||||
2026-10-21 22:48:20.247 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.75,0.25) objectBounds=(-100,-50,200,100) canvas=(160,60) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.298 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.625,0.375) setter=(0.625,0.375) readback=(0.625,0.375) mode=normalized verified=1
|
||||
2026-10-21 22:48:20.298 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.625,0.375) objectBounds=(-100,-50,200,100) canvas=(240,60) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.348 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.25,0.75) setter=(0.25,0.75) readback=(0.25,0.75) mode=normalized verified=1
|
||||
2026-10-21 22:48:20.349 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(144,180) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.398 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 22:48:20.398 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0,1) objectBounds=(-100,-50,200,100) canvas=(100,140) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.447 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(nan,nan) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.500 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 22:48:20.500 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(inf,nan) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.546 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 22:48:20.547 osc-tests[18452:5636580] PGFaceOSC coordinates selection=(0.25,0.75) objectBounds=(-100,-50,200,100) canvas=(80,nan) surface=320x240 origin=0
|
||||
2026-10-21 22:48:20.608 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
2026-10-21 22:48:20.608 osc-tests[18452:5636580] PGFaceOSC point-write desired=(0.5,0.5) setter=(0.5,0.5) readback=(0.5,0.5) mode=normalized verified=1
|
||||
OSC adapter: 98 checks, 0 failures
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"status": "structure_verified_native_loading_pending",
|
||||
"templateSHA256": "e1c0cff1dd3541ed97cab2cb39a76ad4f28dc659b3be7991cc7ac92dd8ea090b",
|
||||
"pluginVersion": "1.1.0",
|
||||
"publications": [
|
||||
{
|
||||
"object": "510000011",
|
||||
"channel": "./2/100",
|
||||
"name": "FCP 版本作者→"
|
||||
},
|
||||
{
|
||||
"object": "510000012",
|
||||
"channel": "./2/100",
|
||||
"name": "更多模板浏览器访问→"
|
||||
},
|
||||
{
|
||||
"object": "510000005",
|
||||
"channel": "./210",
|
||||
"name": ""
|
||||
}
|
||||
],
|
||||
"parameterPathsResolved": 50,
|
||||
"sourceMediaClosed": true,
|
||||
"mediaSHA256": "a7f8e3a2a431dbb3ce9e777040bb55e701cfe9ab9632e8cbb10359732bc8692e",
|
||||
"customState": {
|
||||
"class": "PGInspectorState",
|
||||
"schema": 1,
|
||||
"hasEdits": false,
|
||||
"values": {
|
||||
"41": 0.0,
|
||||
"30": 1.0,
|
||||
"71": 1.0,
|
||||
"27": 0.0,
|
||||
"5": 1.0,
|
||||
"35": 0.0,
|
||||
"24": 0.0,
|
||||
"2": 0.0,
|
||||
"32": 0.0,
|
||||
"40": 1.7999999523162842,
|
||||
"70": 1.0,
|
||||
"18": 0.0,
|
||||
"29": 0.0,
|
||||
"26": 0.0,
|
||||
"15": 0.0,
|
||||
"4": 0.0,
|
||||
"23": 0.0,
|
||||
"12": 0.0,
|
||||
"42": 0.0,
|
||||
"72": 1.0,
|
||||
"31": 0.0,
|
||||
"1": 1.0,
|
||||
"39": 0.0,
|
||||
"28": 0.0,
|
||||
"17": 0.0,
|
||||
"25": 0.0,
|
||||
"14": 0.0,
|
||||
"3": 8.0,
|
||||
"33": 0.0,
|
||||
"22": 0.0
|
||||
},
|
||||
"disabled": [],
|
||||
"classes": [
|
||||
"NSDictionary",
|
||||
"NSSet",
|
||||
"PGInspectorState"
|
||||
],
|
||||
"innerBytes": 935,
|
||||
"innerSHA256": "257540ac6079756a76f3d31dff1b9bca2b33393962057927791316965495ab60",
|
||||
"outerBytes": 1127,
|
||||
"outerSHA256": "95d5f1e2c35c40e9f1234b1480894bcc0ee68c0822c9bd3f7c04338062e6a26b",
|
||||
"encodedCharacters": 1516,
|
||||
"exactTextRoundtrip": true
|
||||
},
|
||||
"nativeLoading": "pending user; no host executed"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"scope": "offscreen AppKit inspector, real production controller and mocked host callbacks; no Motion/FCP UI",
|
||||
"fullChecks": 1039,
|
||||
"counts": {
|
||||
"beauty": 49,
|
||||
"face": 116,
|
||||
"thinChin": 394,
|
||||
"hostAdapterASan": 248,
|
||||
"oscASan": 98,
|
||||
"inspectorASan": 134
|
||||
},
|
||||
"nativeVisualQA": "pending user",
|
||||
"manualAnalysisUI": "removed; legacy IDs hidden and buttons disabled",
|
||||
"renderMode": "realtime automatic follow; stored manual geometry never used",
|
||||
"templatePublishedEntries": 3,
|
||||
"offscreenWidths": [
|
||||
280,
|
||||
320,
|
||||
380,
|
||||
520
|
||||
],
|
||||
"independentViewChecks": 27,
|
||||
"fixedRegressions": [
|
||||
"shape group bypass clears face-box overlay",
|
||||
"disabled legacy detail effects stay disabled on first edit",
|
||||
"legacy global smoothing remains enabled when its old local group was off",
|
||||
"new gesture reads current host after immediate undo",
|
||||
"pending synchronization is bound to CMTime",
|
||||
"same-time host change refreshes without waiting",
|
||||
"explicit numeric input is never skipped due to stale UI cache"
|
||||
],
|
||||
"archives": "Own PGInspectorState secure payload; Foundation outer and inner decode and Motion alphabet roundtrip tested"
|
||||
}
|
||||
+8
@@ -81,3 +81,11 @@ Motion:独立普通视频项目,源PTS0、1920×1080/25fps/10秒,媒体通
|
||||
配套1.0.6效果模板及Drop Zone.tiff已在宿主退出时同步并逐字节核验;moef SHA-256为`30ca8edf36801264c5de2c753c40b720aba8052633620713a12401098fcd2409`。模板仍保留固定UUID及原有参数发布。旧未标记的跟踪数据会回退实时跟随;需要离线重放时请重新分析目标。
|
||||
|
||||
临时Release APP、旧APP备份和公证ZIP已清理。项目、Codex工作树、DerivedData、桌面、下载、Applications、系统/用户FxPlug目录及本次paintfcp临时目录扫描仅发现正式路径外层APP,扫描错误0;没有将这项有界扫描表述为所有卷的完整搜索。完整回执、签名、发现、模板摘要和扫描范围在`verification/v1.0.6/`。最终发布标签为`v1.0.6`,产品源码与签名构建节点ad54726一致;其后的提交仅更新交付文档、模板说明和验证回执。
|
||||
|
||||
## 1.1.0 原生卡片面板(构建前)
|
||||
|
||||
按参考界面的深色圆角卡片、青/绿/蓝紫分组、自绘滑块与开关、肤色圆点和单项重置实现独立AppKit面板,使用大鱼自己的品牌文字。新增自定义参数210及NSSecureCoding状态对象;FCP发布由48入口缩为面板+版权两项共3入口,旧49项参数和组路径保留隐藏兼容,手动分析命令隐藏且禁用。实时渲染不再载入任何保存的手动分析点阵,也不带旧分析大数据进入render snapshot。
|
||||
|
||||
完整1039项通过(美化49、人脸116、瘦脸/下巴394、宿主适配248、OSC98、检查器134),后三类适配/检查器使用ASan。检查器覆盖单项路由、完整安全归档、Motion自定义包内实际inner/outer的Foundation解码、初始不覆盖旧文档、数值插值、启停保值、重置、失效写入、事务配对、快速撤销/切时间后新编辑、显式数值输入与窄宽度布局。独立审查另用mock验证27项视图交互;280/320/380/520点宽度及更多展开不越界。测试修复了分组关闭后框仍显示、旧细节禁用状态丢失、快速撤销/换帧旧值回灌和旧显示缓存拦截明确输入等问题。
|
||||
|
||||
参考原版仅用于静态结构和外观分析,自有归档不含原版类/私有数据/品牌图。面板PNG为离屏演示参数预览,不是FCP原生截图。尚未操作Motion/FCP界面,真实宿主嵌入滚动、键盘/VoiceOver及工程重开由用户自测。源码与回执会按正式交付节点保存。
|
||||
@@ -8,6 +8,8 @@ compiler_extra=()
|
||||
if [[ "${PAINTFCP_INSPECTOR_ASAN:-0}" == 1 ]]; then
|
||||
compiler_extra=(-fsanitize=address -g)
|
||||
fi
|
||||
python3 scripts/inspector-template.py audit 'Templates/大鱼人像美化/大鱼人像美化.moef' \
|
||||
--inner-output "$output_dir/template-inner.bplist" --outer-output "$output_dir/template-outer.bplist" > /dev/null
|
||||
xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unused-parameter \
|
||||
-DCI_SILENCE_GL_DEPRECATION -mmacosx-version-min=13.0 -I "$plugin_dir" \
|
||||
-F /Library/Developer/SDKs/FxPlug.sdk/Library/Frameworks -F /Library/Developer/Frameworks \
|
||||
@@ -19,4 +21,4 @@ xcrun clang "${compiler_extra[@]}" -fobjc-arc -fmodules -Wall -Wextra -Wno-unuse
|
||||
-framework CoreMedia -framework CoreVideo -framework IOSurface -framework Metal \
|
||||
-framework Vision -framework ImageIO -framework FxPlug -framework PluginManager \
|
||||
-o "$output_dir/inspector-tests"
|
||||
"$output_dir/inspector-tests"
|
||||
"$output_dir/inspector-tests" "$output_dir/template-inner.bplist" "$output_dir/template-outer.bplist"
|
||||
@@ -409,7 +409,7 @@ static void TrackingCoordinateTests(NSDictionary *archive, NSDictionary *hostVal
|
||||
PGHostAPI *trustedAPI = [PGHostAPI new]; trustedAPI.hostTime = time;
|
||||
beatygrilPlugIn *trusted = [[beatygrilPlugIn alloc] initWithAPIManager:trustedAPI]; [trusted addParametersWithError:nil];
|
||||
trustedAPI.values = [values mutableCopy]; trustedAPI.values[@(PGTrackData3)] = [trustedTrack base64EncodedStringWithOptions:0];
|
||||
Check(Render(trusted, output, @[source], Snapshot(trusted,time), time, nil, NULL), @"fresh plugin restores a marked coordinate archive");
|
||||
Check(Render(trusted, output, @[source], Snapshot(trusted,time), time, nil, NULL), @"fresh plugin accepts a snapshot with marked legacy analysis data");
|
||||
NSData *trustedBytes = SurfaceBytes(output);
|
||||
PGBeautySettings tracked = [trusted settingsAtTime:time]; tracked.trackingEnabled=1;
|
||||
NSData *expectedSaved = ImageBytes(context, ProcessedReference(portrait,tracked,time,trustedTrack));
|
||||
@@ -545,7 +545,7 @@ int main(int argc, const char *argv[]) {
|
||||
api.values[@(PGSelection)] = @[@0.43, @0.76];
|
||||
NSData *oldRenderState = Snapshot(plugin, api.hostTime);
|
||||
[plugin analyzeForward];
|
||||
Check(api.forwardRequests == 1, @"forward analyze button calls host analysis API");
|
||||
Check(api.forwardRequests == 1, @"hidden legacy forward-analysis method still accepts old document callbacks");
|
||||
CMTimeRange desired, clip = CMTimeRangeMake(kCMTimeZero, CMTimeMake(240, 24));
|
||||
[plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error];
|
||||
Check(CMTimeCompare(desired.start, api.hostTime) == 0 && CMTimeCompare(CMTimeRangeGetEnd(desired), CMTimeRangeGetEnd(clip)) == 0, @"analysis seed comes from current host playhead time");
|
||||
@@ -569,7 +569,7 @@ int main(int argc, const char *argv[]) {
|
||||
Check([api.values[@(PGTrackEnabled)] boolValue] && CMTimeCompare(api.lastSetBoolTime, api.hostTime) == 0, @"analysis enables saved target at host seed time");
|
||||
NSData *trackedState = Snapshot(plugin, api.hostTime);
|
||||
beatygrilPlugIn *restoredPlugin = [[beatygrilPlugIn alloc] initWithAPIManager:api];
|
||||
Check(Render(restoredPlugin, portraitOutput, @[portraitSource], trackedState, api.hostTime, &error, NULL), @"fresh plugin instance restores target data from serialized host snapshot");
|
||||
Check(Render(restoredPlugin, portraitOutput, @[portraitSource], trackedState, api.hostTime, &error, NULL), @"fresh plugin instance uses automatic follow despite serialized legacy analysis settings");
|
||||
[plugin analyzeBackward];
|
||||
Check(api.backwardRequests == 1, @"backward analyze button calls host API");
|
||||
[plugin desiredAnalysisTimeRange:&desired forInputWithTimeRange:clip error:&error];
|
||||
|
||||
+39
-3
@@ -71,9 +71,9 @@ static void ModelTests(void) {
|
||||
NSDictionary *bad=@{@(PGChin):@9,@(PGTonePreset):@(-2),@(PGBrightness):@(NAN),@999:@.5};
|
||||
PGInspectorState *safe=[PGInspectorState stateWithValues:bad disabledParameters:[NSSet setWithObjects:@(PGRadius),@999,nil] hasEdits:YES];
|
||||
Check([safe valueForParameter:PGChin]==1 && [safe valueForParameter:PGTonePreset]==0 && [safe valueForParameter:PGBrightness]==0 && safe.values[@999]==nil && safe.disabledParameters.count==0,@"malformed controls are clamped, finite and limited to the supported schema");
|
||||
NSMutableDictionary *off=[a.values mutableCopy];off[@(PGShapeEnabled)]=@0;off[@(PGColorEnabled)]=@0;off[@(PGSmoothingEnabled)]=@0;
|
||||
NSMutableDictionary *off=[a.values mutableCopy];off[@(PGShapeEnabled)]=@0;off[@(PGColorEnabled)]=@0;off[@(PGSmoothingEnabled)]=@0;off[@(PGTrackBoxes)]=@1;
|
||||
s=PGSettingsWithInspectorState(PGBeautyDefaults(),[PGInspectorState stateWithValues:off disabledParameters:[NSSet set] hasEdits:YES]);
|
||||
Check(s.faceChin==0 && s.faceBigEye==0 && s.skinBrightness==0 && s.skinTonePreset==0 && s.midtoneDetail==0 && s.highPassDetail==0,@"card eye switches bypass the complete associated effect group");
|
||||
Check(s.faceChin==0 && s.faceBigEye==0 && s.skinBrightness==0 && s.skinTonePreset==0 && s.midtoneDetail==0 && s.highPassDetail==0 && s.showTrackBox==0,@"card eye switches bypass the complete associated effect group");
|
||||
}
|
||||
static void ControllerTests(void) {
|
||||
PGInspectorTestAPI *api=[PGInspectorTestAPI new];beatygrilPlugIn *plugin=[[beatygrilPlugIn alloc]initWithAPIManager:api];[plugin addParametersWithError:nil];
|
||||
@@ -98,6 +98,27 @@ static void ControllerTests(void) {
|
||||
api.values=[undo mutableCopy];[[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.36]];[c refresh];
|
||||
Check(Near([c.state valueForParameter:PGChin],[old valueForParameter:PGChin]),@"host undo refresh restores the prior committed state after pending synchronization expires");
|
||||
Check(api.accessViolations==0 && api.actionDepth==0 && api.actionStarts==api.actionEnds,@"all arbitrary-time reads and writes are inside balanced FxPlug action pairs");
|
||||
[c setValue:.7 forParameter:PGBrightness];NSDictionary *beforeUndo=api.undoValues;
|
||||
api.values=[beforeUndo mutableCopy];[c setValue:.2 forParameter:PGThin];
|
||||
PGInspectorState *afterImmediateUndo=api.values[@(PGInspectorPanel)];
|
||||
PGInspectorState *undoneState=beforeUndo[@(PGInspectorPanel)];
|
||||
Check(Near([afterImmediateUndo valueForParameter:PGBrightness],[undoneState valueForParameter:PGBrightness]) && Near([afterImmediateUndo valueForParameter:PGThin],.2),@"a new gesture immediately after host undo cannot resurrect the undone value");
|
||||
NSMutableDictionary *seekValues=[c.state.values mutableCopy];seekValues[@(PGChin)]=@(-.8);seekValues[@(PGMidtone)]=@.6;
|
||||
api.values[@(PGInspectorPanel)]=[PGInspectorState stateWithValues:seekValues disabledParameters:[NSSet set] hasEdits:YES];api.hostTime=CMTimeAdd(api.hostTime,CMTimeMake(1,24));[c refresh];
|
||||
Check(Near([c.state valueForParameter:PGChin],-.8) && Near([c.state valueForParameter:PGMidtone],.6),@"seeking invalidates pending display state immediately at the new CMTime");
|
||||
[c setValue:.1 forParameter:PGBrightness];Check(Near([c.state valueForParameter:PGChin],-.8),@"new-time edits preserve the other controls from that time instead of a stale prior frame");
|
||||
api.values=[api.undoValues mutableCopy];[c documentDidChange];
|
||||
Check(Near([c.state valueForParameter:PGBrightness],[seekValues[@(PGBrightness)]doubleValue]),@"a host undo notification refreshes same-time state immediately without a timed wait");
|
||||
PGHostAPI *legacy=[PGHostAPI new];beatygrilPlugIn *legacyPlugin=[[beatygrilPlugIn alloc]initWithAPIManager:legacy];[legacyPlugin addParametersWithError:nil];
|
||||
legacy.values[@(PGDetailEnabled)]=@NO;legacy.values[@(PGHighPass)]=@.6;legacy.values[@(PGSharpness)]=@.4;
|
||||
PGInspectorController *imported=[[PGInspectorController alloc]initWithAPIManager:legacy];[imported refresh];[imported setValue:.3 forParameter:PGBrightness];
|
||||
PGBeautySettings migrated=PGSettingsWithInspectorState(PGBeautyDefaults(),imported.state);
|
||||
Check(migrated.highPassDetail==0 && migrated.sharpness==0 && Near([imported.state valueForParameter:PGHighPass],.6),@"first panel edit preserves disabled legacy detail controls and their stored amounts");
|
||||
legacy.values[@(PGInspectorPanel)]=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];
|
||||
legacy.values[@(PGDetailEnabled)]=@YES;legacy.values[@(PGSmoothingEnabled)]=@NO;
|
||||
PGInspectorController *split=[[PGInspectorController alloc]initWithAPIManager:legacy];[split refresh];[split setValue:.2 forParameter:PGBrightness];
|
||||
PGBeautySettings splitSettings=PGSettingsWithInspectorState(PGBeautyDefaults(),split.state);
|
||||
Check(Near(splitSettings.highPassDetail,.6) && splitSettings.amount==0 && splitSettings.midtoneDetail==0,@"legacy global smoothing remains active when its old local-skin group was disabled");
|
||||
Check(api.forwardRequests==0 && api.backwardRequests==0,@"normal inspector edits never request manual analysis");
|
||||
[api.disabledAPIs addObject:@"FxCustomParameterActionAPI_v4"];
|
||||
PGInspectorController *offline=[[PGInspectorController alloc]initWithAPIManager:api];[offline refresh];
|
||||
@@ -111,6 +132,10 @@ static void ViewInteractionTests(void) {
|
||||
Check(Near([s valueForParameter:PGChin],-.75) && [s valueForParameter:PGBigEye]==0,@"actual chin slider events do not write the eye parameter");
|
||||
NSTextField *number=(NSTextField *)FindControl(view,NSTextField.class,@"下巴数值");number.stringValue=@"0.40";Fire(number);s=api.values[@(PGInspectorPanel)];Check(Near([s valueForParameter:PGChin],.4),@"editable numeric field writes its signed control");
|
||||
number.stringValue=@"invalid";Fire(number);s=api.values[@(PGInspectorPanel)];Check(Near([s valueForParameter:PGChin],.4),@"invalid number entry preserves the last valid value");
|
||||
NSMutableDictionary *changed=[s.values mutableCopy];changed[@(PGChin)]=@.8;
|
||||
api.values[@(PGInspectorPanel)]=[PGInspectorState stateWithValues:changed disabledParameters:s.disabledParameters hasEdits:YES];
|
||||
number.stringValue=@"0.40";Fire(number);s=api.values[@(PGInspectorPanel)];
|
||||
Check(Near([s valueForParameter:PGChin],.4),@"explicit input matching old UI cache still overrides a newer host value");
|
||||
NSButton *preview=(NSButton *)FindControl(view,NSButton.class,@"人脸框预览");preview.state=NSControlStateValueOn;Fire(preview);s=api.values[@(PGInspectorPanel)];Check([s valueForParameter:PGTrackBoxes]==1,@"preview switch events change the automatic face box setting");
|
||||
for(NSNumber *width in @[@280,@320,@380,@520]) {
|
||||
[view setFrameSize:NSMakeSize(width.doubleValue,20)];[view layoutSubtreeIfNeeded];
|
||||
@@ -118,8 +143,19 @@ static void ViewInteractionTests(void) {
|
||||
for(NSView *card in view.subviews)if(!card.hidden)Check(NSMaxY(card.frame)<=NSHeight(view.bounds)+1 && NSMaxX(card.frame)<=NSWidth(view.bounds)+1,@"every visible card and footer remains inside the panel at all inspector widths");
|
||||
}
|
||||
}
|
||||
int main(void) {@autoreleasepool {
|
||||
int main(int argc,const char **argv) {@autoreleasepool {
|
||||
[NSApplication sharedApplication];[NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
|
||||
RegistrationTests();ModelTests();ControllerTests();ViewInteractionTests();
|
||||
if(argc>2) {
|
||||
NSData *inner=[NSData dataWithContentsOfFile:@(argv[1])],*outer=[NSData dataWithContentsOfFile:@(argv[2])];NSError *error=nil;
|
||||
NSKeyedUnarchiver *reader=[[NSKeyedUnarchiver alloc]initForReadingFromData:inner error:&error];reader.requiresSecureCoding=YES;
|
||||
PGInspectorState *state=[reader decodeObjectOfClass:PGInspectorState.class forKey:@"Custom Data"];[reader finishDecoding];
|
||||
PGInspectorState *expected=[PGInspectorState stateWithValues:@{} disabledParameters:[NSSet set] hasEdits:NO];
|
||||
Check(!error && !reader.error && [state isEqual:expected],@"the template's actual Custom Data bytes securely decode to our untouched state class");
|
||||
NSKeyedUnarchiver *envelope=[[NSKeyedUnarchiver alloc]initForReadingFromData:outer error:&error];envelope.requiresSecureCoding=YES;
|
||||
NSData *payload=[envelope decodeObjectOfClass:NSData.class forKey:@"BlindDataObject"];
|
||||
NSNumber *legacy=[envelope decodeObjectOfClass:NSNumber.class forKey:@"DataIsLegacy"];[envelope finishDecoding];
|
||||
Check(!error && !envelope.error && legacy && !legacy.boolValue && [payload isEqual:inner],@"Foundation reads the Motion envelope as the exact secure custom payload");
|
||||
}
|
||||
printf("Inspector: %lu checks, %lu failures\n",(unsigned long)Checks,(unsigned long)Failures);return Failures?1:0;
|
||||
}}
|
||||
在新工单中引用
屏蔽一个用户